Merge branch 'master' into develop
This commit is contained in:
Generated
+291
-292
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,11 @@ framework:
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
handler_id: session.handler.native_file
|
||||
cookie_secure: auto
|
||||
cookie_secure: true
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
|
||||
name: epsession
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
@@ -32,6 +32,7 @@ security:
|
||||
token_provider:
|
||||
doctrine: true
|
||||
access_control:
|
||||
- { path: '^/_uploader', roles: ROLE_USER, requires_channel: https }
|
||||
- { path: '^/', roles: PUBLIC_ACCESS, requires_channel: https }
|
||||
|
||||
when@test:
|
||||
|
||||
@@ -53,6 +53,11 @@ services:
|
||||
$connection: '@doctrine.dbal.default_connection'
|
||||
$xmlFilesPath: '%kernel.project_dir%/xmlexport'
|
||||
|
||||
App\Command\GenerateThumbnailsCommand:
|
||||
arguments:
|
||||
$cacheManager: '@liip_imagine.cache.manager'
|
||||
$filterService: '@liip_imagine.service.filter'
|
||||
|
||||
App\BusProNet\ApiClient:
|
||||
arguments:
|
||||
$logger: '@monolog.logger.bpn'
|
||||
|
||||
@@ -92,6 +92,7 @@ task('deploy', [
|
||||
'database:migrate',
|
||||
'deploy:publish',
|
||||
'cachetool:clear:opcache',
|
||||
// 'deploy:thumbnails',
|
||||
// 'deploy:stop-workers',
|
||||
]);
|
||||
|
||||
@@ -103,4 +104,8 @@ task('deploy:assets', function () {
|
||||
runLocally('npm i && npm run build');
|
||||
});
|
||||
|
||||
task('deploy:thumbnails', function () {
|
||||
run('{{bin/console}} app:generate-thumbnails');
|
||||
});
|
||||
|
||||
after('deploy:failed', 'deploy:unlock');
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||
use Liip\ImagineBundle\Service\FilterService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:generate-thumbnails',
|
||||
description: 'Generates thumbnails of profile images',
|
||||
)]
|
||||
class GenerateThumbnailsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly CacheManager $cacheManager,
|
||||
private readonly FilterService $filterService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$images = $this
|
||||
->entityManager
|
||||
->getRepository(Teamer::class)
|
||||
->createQueryBuilder('t')
|
||||
->select('t.id', 'p.filename')
|
||||
->innerJoin('t.photo', 'p')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
$imagesCount = count($images);
|
||||
|
||||
$io->info('Generating thumbnails for '.$imagesCount.' profile images...');
|
||||
|
||||
$progressbar = $io->createProgressBar($imagesCount);
|
||||
$progressbar->start();
|
||||
|
||||
foreach ($images as $image) {
|
||||
try {
|
||||
$this->filterService->warmUpCache($image['filename'], 'thumbnail');
|
||||
$this->filterService->warmUpCache($image['filename'], 'profile');
|
||||
$this->cacheManager->getBrowserPath($image['filename'], 'thumbnail');
|
||||
$this->cacheManager->getBrowserPath($image['filename'], 'profile');
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
$progressbar->advance();
|
||||
}
|
||||
|
||||
$progressbar->finish();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class IndexController extends AbstractController
|
||||
$dispositionRepository = $this->entityManager->getRepository(Disposition::class);
|
||||
$overdueContracts = $dispositionRepository->findOverdueContracts();
|
||||
$newDispositions = $dispositionRepository->getNew();
|
||||
$overdueFeedbacks = $dispositionRepository->findDispositionsWithOverdueFeedback(7);
|
||||
|
||||
return $this->render('admin/index.html.twig', [
|
||||
'applications' => $applications,
|
||||
@@ -59,6 +60,7 @@ class IndexController extends AbstractController
|
||||
'pendingDocumentsCount' => $pendingDocumentsCount,
|
||||
'overdueContracts' => $overdueContracts,
|
||||
'newDispositions' => $newDispositions,
|
||||
'overdueFeedbacks' => $overdueFeedbacks,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Controller\Administrative\Teamer;
|
||||
use App\Entity\Teamer;
|
||||
use App\Repository\DispositionRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
@@ -15,7 +16,7 @@ class InfoController extends AbstractController
|
||||
{}
|
||||
|
||||
#[Route('/administrative/teamer/info/{uuid}', name: 'app_administrative_teamer_info')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
#[IsGranted(new Expression('is_granted("ROLE_ADMINISTRATIVE") or is_granted("ROLE_HOUSE_MANAGER")'))]
|
||||
public function index(Teamer $teamer): Response
|
||||
{
|
||||
$recentDispositions = $this->dispositionRepository->findRecentDispositionsByTeamer($teamer);
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Controller\HouseManager\Feedback;
|
||||
|
||||
use App\BusProNet\DataProvider\HotelDataProvider;
|
||||
use App\BusProNet\Model\Hotel;
|
||||
use App\Entity\User;
|
||||
use App\Repository\DispositionRepository;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
@@ -17,7 +15,6 @@ class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DispositionRepository $dispositionRepository,
|
||||
private readonly HotelDataProvider $hotelDataProvider,
|
||||
private readonly PaginatorInterface $paginator
|
||||
) {
|
||||
}
|
||||
@@ -29,18 +26,9 @@ class IndexController extends AbstractController
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$hotels = $this
|
||||
->hotelDataProvider
|
||||
->findByCode($user->getHotelCode())
|
||||
;
|
||||
|
||||
$hotelBusProIds = array_map(function (Hotel $hotel) {
|
||||
return $hotel->getBusProId();
|
||||
}, $hotels);
|
||||
|
||||
$query = $this
|
||||
->dispositionRepository
|
||||
->getDispositionsWithPendingFeedbackQuery($hotelBusProIds)
|
||||
->getDispositionsWithPendingFeedbackQuery($user->getHotelCode())
|
||||
;
|
||||
|
||||
$pagination = $this->paginator->paginate(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Controller\HouseManager;
|
||||
|
||||
use App\BusProNet\DataProvider\HotelDataProvider;
|
||||
use App\BusProNet\Model\Hotel;
|
||||
use App\Entity\User;
|
||||
use App\Repository\DispositionRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -31,18 +30,14 @@ class IndexController extends AbstractController
|
||||
->findByCode($user->getHotelCode())
|
||||
;
|
||||
|
||||
$hotelBusProIds = array_map(function (Hotel $hotel) {
|
||||
return $hotel->getBusProId();
|
||||
}, $hotels);
|
||||
|
||||
$newDispositions = $this
|
||||
->dispositionRepository
|
||||
->findNewDispositionsByHotelBusProIds($hotelBusProIds)
|
||||
->findNewDispositionsByHotelCode($user->getHotelCode())
|
||||
;
|
||||
|
||||
$pendingFeedbacks = $this
|
||||
->dispositionRepository
|
||||
->findDispositionsWithPendingFeedbackByHotelBusProIds($hotelBusProIds)
|
||||
->findDispositionsWithPendingFeedbackByHotelCode($user->getHotelCode())
|
||||
;
|
||||
|
||||
return $this->render('house_manager/index.html.twig', [
|
||||
|
||||
@@ -10,6 +10,7 @@ use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
|
||||
class ContractPdfController extends AbstractController
|
||||
{
|
||||
@@ -26,12 +27,19 @@ class ContractPdfController extends AbstractController
|
||||
{
|
||||
$pdf = $this->renderer->render($disposition);
|
||||
|
||||
$teamer = $disposition->getTeamer();
|
||||
$assignment = $disposition->getAssignment();
|
||||
$destination = $assignment->getDestination();
|
||||
$slugger = new AsciiSlugger('de');
|
||||
|
||||
$filename = sprintf('Honorarvertrag_%s_%s.pdf', $teamer, $destination);
|
||||
$disposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename, md5($filename));
|
||||
$teamer = $disposition->getTeamer();
|
||||
$teamerPart = $slugger->slug($teamer);
|
||||
$assignment = $disposition->getAssignment();
|
||||
$destination = $slugger->slug($assignment->getDestination());
|
||||
|
||||
$filename = sprintf('Honorarvertrag_%s_%s.pdf', $teamerPart, $destination);
|
||||
$disposition = HeaderUtils::makeDisposition(
|
||||
HeaderUtils::DISPOSITION_ATTACHMENT,
|
||||
$filename,
|
||||
md5($filename)
|
||||
);
|
||||
|
||||
$response = new Response($pdf->Output('S'));
|
||||
$response->setPrivate();
|
||||
|
||||
@@ -10,6 +10,7 @@ use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
|
||||
class InvoicePdfController extends AbstractController
|
||||
{
|
||||
@@ -26,12 +27,19 @@ class InvoicePdfController extends AbstractController
|
||||
{
|
||||
$pdf = $this->renderer->render($disposition);
|
||||
|
||||
$assignment = $disposition->getAssignment();
|
||||
$destination = $assignment->getDestination();
|
||||
$teamer = $disposition->getTeamer();
|
||||
$slugger = new AsciiSlugger('de');
|
||||
|
||||
$filename = sprintf('Honorarnote_%s_%s.pdf', $teamer, $disposition->getAssignment()->getDestination());
|
||||
$contentDisposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename, md5($filename));
|
||||
$teamer = $disposition->getTeamer();
|
||||
$teamerPart = $slugger->slug($teamer);
|
||||
$assignment = $disposition->getAssignment();
|
||||
$destination = $slugger->slug($assignment->getDestination());
|
||||
|
||||
$filename = sprintf('Honorarnote_%s_%s.pdf', $teamerPart, $disposition->getAssignment()->getDestination());
|
||||
$contentDisposition = HeaderUtils::makeDisposition(
|
||||
HeaderUtils::DISPOSITION_ATTACHMENT,
|
||||
$filename,
|
||||
md5($filename)
|
||||
);
|
||||
|
||||
$response = new Response($pdf->Output('S'));
|
||||
$response->setPrivate();
|
||||
|
||||
@@ -55,7 +55,6 @@ class IndexController extends AbstractController
|
||||
|
||||
$form = $this->createForm(TeamerProfileType::class, $teamer, [
|
||||
'upload_session' => $uploadSession,
|
||||
'admin_mode' => $this->isGranted('IS_IMPERSONATOR'),
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
|
||||
@@ -175,8 +175,12 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPickup(int $id): ?Pickup
|
||||
public function getPickup(?int $id): ?Pickup
|
||||
{
|
||||
if (null === $id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->getPickups() as $pickup) {
|
||||
if ($id === $pickup['busProId']) {
|
||||
return Pickup::fromArray($pickup);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Application;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\ApplicationStatusEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\Model\ApplicationStatusDto;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[AsEventListener(event: DocumentConfirmedEvent::NAME, method: 'onDocumentConfirmed')]
|
||||
class RejectApplicationListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onDocumentConfirmed(DocumentConfirmedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
if (Upload::TYPE_CONTRACT !== $document->getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$disposition = $document->getDisposition();
|
||||
$teamer = $disposition->getTeamer();
|
||||
$otherApplications = $disposition
|
||||
->getAssignment()
|
||||
->getApplications()
|
||||
->filter(function (Application $application) use ($teamer) {
|
||||
return $application->getTeamer() !== $teamer;
|
||||
})
|
||||
;
|
||||
|
||||
foreach ($otherApplications as $application) {
|
||||
$application
|
||||
->setStatus(Application::STATUS_REJECTED)
|
||||
->setComment('Abgelehnt, da Einsatz bereits anderweitig besetzt')
|
||||
;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
foreach ($otherApplications as $application) {
|
||||
$statusDto = new ApplicationStatusDto($application);
|
||||
$this->eventDispatcher->dispatch(
|
||||
new ApplicationStatusEvent($statusDto),
|
||||
ApplicationStatusEvent::NAME
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -27,6 +28,7 @@ class AssignmentFilterType extends AbstractType
|
||||
$builder
|
||||
->add('dateFrom', DatepickerType::class, [
|
||||
'label' => 'Zeitraum von',
|
||||
'required' => false,
|
||||
'min_date' => $options['min_date'],
|
||||
'max_date' => $options['max_date'],
|
||||
'attr' => [
|
||||
@@ -35,6 +37,7 @@ class AssignmentFilterType extends AbstractType
|
||||
])
|
||||
->add('dateTo', DatepickerType::class, [
|
||||
'label' => 'Zeitraum bis',
|
||||
'required' => false,
|
||||
'min_date' => $options['min_date'],
|
||||
'max_date' => $options['max_date'],
|
||||
'attr' => [
|
||||
@@ -65,17 +68,23 @@ class AssignmentFilterType extends AbstractType
|
||||
;
|
||||
|
||||
if ($this->security->isGranted('ROLE_ADMINISTRATIVE')) {
|
||||
$builder->add('status', MultiselectType::class, [
|
||||
'label' => 'Status',
|
||||
'required' => false,
|
||||
'empty_label' => 'nicht filtern',
|
||||
'choices' => [
|
||||
'voll besetzt' => Assignment::STATUS_STAFFED,
|
||||
'teilweise besetzt' => Assignment::STATUS_PARTLY_STAFFED,
|
||||
'unbesetzt' => Assignment::STATUS_UNSTAFFED,
|
||||
'mit Bewerbungen' => Assignment::STATUS_STAFFING,
|
||||
],
|
||||
]);
|
||||
$builder
|
||||
->add('id', IntegerType::class, [
|
||||
'label' => 'ID',
|
||||
'required' => false,
|
||||
])
|
||||
->add('status', MultiselectType::class, [
|
||||
'label' => 'Status',
|
||||
'required' => false,
|
||||
'empty_label' => 'nicht filtern',
|
||||
'choices' => [
|
||||
'voll besetzt' => Assignment::STATUS_STAFFED,
|
||||
'teilweise besetzt' => Assignment::STATUS_PARTLY_STAFFED,
|
||||
'unbesetzt' => Assignment::STATUS_UNSTAFFED,
|
||||
'mit Bewerbungen' => Assignment::STATUS_STAFFING,
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
if (0 < count($options['job_profiles'])) {
|
||||
|
||||
@@ -79,6 +79,7 @@ class TeamerProfileType extends AbstractType
|
||||
'entry_type' => BpnPickupType::class,
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'delete_empty' => true,
|
||||
])
|
||||
->add('language', TextType::class, [
|
||||
'label' => 'Sprache(n)',
|
||||
|
||||
@@ -12,6 +12,7 @@ class AssignmentFilterDto extends AbstractFilterDto
|
||||
public const DURATION_FULL_WEEK = 3;
|
||||
public const DURATION_MORE = 4;
|
||||
|
||||
protected ?int $id = null;
|
||||
protected ?\DateTimeImmutable $dateFrom = null;
|
||||
protected ?\DateTimeImmutable $dateTo = null;
|
||||
protected array $jobProfiles = [];
|
||||
@@ -20,6 +21,18 @@ class AssignmentFilterDto extends AbstractFilterDto
|
||||
protected array $status = [];
|
||||
protected bool $includePast = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(?int $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateFrom(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->dateFrom;
|
||||
|
||||
@@ -85,6 +85,13 @@ class AssignmentRepository extends ServiceEntityRepository
|
||||
|
||||
private function applyFilterSettings(AssignmentFilterDto $filterDto, QueryBuilder $qb): void
|
||||
{
|
||||
if (null !== $filterDto->getId()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('assignment.id', ':id'))
|
||||
->setParameter('id', $filterDto->getId())
|
||||
;
|
||||
}
|
||||
|
||||
if (false === $filterDto->isIncludePast()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->gte('destination.dateFrom', ':now'))
|
||||
|
||||
@@ -5,10 +5,8 @@ namespace App\Repository;
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\Upload;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
@@ -150,7 +148,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
public function findNewDispositionsByHotelBusProIds(array $hotelBusProIds): array
|
||||
public function findNewDispositionsByHotelCode(string $hotelCode): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
@@ -159,12 +157,12 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->in('destination.hotelBusProId', ':hotelBusProIds'),
|
||||
$qb->expr()->like('destination.hotelCode', ':hotelCode'),
|
||||
$qb->expr()->gte('destination.dateTo', ':dateTo'),
|
||||
$qb->expr()->eq('disposition.status', ':status')
|
||||
))
|
||||
->orderBy('destination.dateFrom', 'ASC')
|
||||
->setParameter('hotelBusProIds', $hotelBusProIds)
|
||||
->setParameter('hotelCode', '%'.$hotelCode.'%')
|
||||
->setParameter('dateTo', new \DateTimeImmutable())
|
||||
->setParameter('status', Disposition::STATUS_CONFIRMED)
|
||||
->getQuery()
|
||||
@@ -172,7 +170,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
public function getDispositionsWithPendingFeedbackQuery(array $hotelBusProIds = null): Query
|
||||
public function getDispositionsWithPendingFeedbackQuery(?string $hotelCode = null, ?int $offsetDays = null): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
@@ -182,6 +180,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->leftJoin('disposition.feedback', 'feedback')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->notIn('assignment.status', ':status'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
@@ -191,31 +190,37 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
),
|
||||
$qb->expr()->isNull('feedback')
|
||||
))
|
||||
->setParameter('dateTo', new \DateTimeImmutable())
|
||||
->setParameter('status', [Assignment::STATUS_CALLED_OFF, Assignment::STATUS_DELETED])
|
||||
;
|
||||
|
||||
if (null !== $hotelBusProIds) {
|
||||
$dateTo = new \DateTimeImmutable();
|
||||
if (null !== $offsetDays) {
|
||||
$dateTo = $dateTo->modify('-'.$offsetDays.' days');
|
||||
}
|
||||
$qb->setParameter('dateTo', $dateTo);
|
||||
|
||||
if (null !== $hotelCode) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->in('destination.hotelBusProId', ':hotelBusProIds'))
|
||||
->setParameter('hotelBusProIds', $hotelBusProIds)
|
||||
->andWhere($qb->expr()->like('destination.hotelCode', ':hotelCode'))
|
||||
->setParameter('hotelCode', '%'.$hotelCode.'%')
|
||||
;
|
||||
}
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
public function findDispositionsWithPendingFeedback(): array
|
||||
public function findDispositionsWithOverdueFeedback(?int $offsetDays = null): array
|
||||
{
|
||||
return $this
|
||||
->getDispositionsWithPendingFeedbackQuery()
|
||||
->getDispositionsWithPendingFeedbackQuery(null, $offsetDays)
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function findDispositionsWithPendingFeedbackByHotelBusProIds(array $hotelBusProIds): array
|
||||
public function findDispositionsWithPendingFeedbackByHotelCode(string $hotelCode): array
|
||||
{
|
||||
return $this
|
||||
->getDispositionsWithPendingFeedbackQuery($hotelBusProIds)
|
||||
->getDispositionsWithPendingFeedbackQuery($hotelCode)
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
@@ -279,15 +284,15 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
->select('disposition', 'assignment', 'destination', 'document')
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->leftJoin('disposition.documents', 'document', Join::WITH, 'document.type = :documentType')
|
||||
->leftJoin('disposition.documents', 'document')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->lte('disposition.createdAt', ':dueDate'),
|
||||
$qb->expr()->eq('disposition.status', ':status'),
|
||||
$qb->expr()->isNull('document')
|
||||
))
|
||||
->setParameter('documentType', Upload::TYPE_CONTRACT)
|
||||
->setParameter('dueDate', $dueDate)
|
||||
->setParameter('status', Disposition::STATUS_CONFIRMED)
|
||||
->setParameter('status', Disposition::STATUS_NEW)
|
||||
->orderBy('disposition.createdAt', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
@@ -54,12 +54,14 @@ class ApplicationVoter extends Voter
|
||||
|
||||
if ($this->security->isGranted('ROLE_TEAMER') && in_array($attribute, $teamerAttributes)) {
|
||||
$teamer = $user->getTeamer();
|
||||
|
||||
if ($teamer !== $application->getTeamer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($attribute) {
|
||||
static::WITHDRAW => $teamer === $application->getTeamer(),
|
||||
static::DELETE => $teamer === $application->getTeamer()
|
||||
&& Application::STATUS_REJECTED === $status,
|
||||
static::VIEW => $teamer === $application->getTeamer()
|
||||
&& Application::STATUS_REJECTED !== $status,
|
||||
static::DELETE => Application::STATUS_REJECTED === $status,
|
||||
static::VIEW, static::WITHDRAW => Application::STATUS_REJECTED !== $status,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ class AssignmentFilterHandler extends AbstractFilterHandler
|
||||
|
||||
$filterDto = new AssignmentFilterDto();
|
||||
|
||||
if (isset($data['id'])) {
|
||||
$filterDto->setId($data['id']);
|
||||
}
|
||||
if (isset($data['date_from'])) {
|
||||
$filterDto->setDateFrom($data['date_from']);
|
||||
}
|
||||
@@ -52,6 +55,7 @@ class AssignmentFilterHandler extends AbstractFilterHandler
|
||||
{
|
||||
/** @var AssignmentFilterDto $filterDto */
|
||||
$this->getSession()->set($this->namespace, [
|
||||
'id' => $filterDto->getId(),
|
||||
'date_from' => $filterDto->getDateFrom(),
|
||||
'date_to' => $filterDto->getDateTo(),
|
||||
'job_profiles' => array_map(function (JobProfile $jobProfile) {
|
||||
|
||||
@@ -24,7 +24,7 @@ class FeedbackReminderService
|
||||
public function sendFeedbackReminders(): string
|
||||
{
|
||||
/** @var Disposition[] $dispositionsWithPendingFeedbacks */
|
||||
$dispositionsWithPendingFeedbacks = $this->dispositionRepository->findDispositionsWithPendingFeedback();
|
||||
$dispositionsWithPendingFeedbacks = $this->dispositionRepository->findDispositionsWithOverdueFeedback();
|
||||
|
||||
$sortedFeedbacks = [];
|
||||
|
||||
|
||||
@@ -23,11 +23,14 @@ class PickupResolver
|
||||
return false;
|
||||
}
|
||||
|
||||
$teamerPickups = array_map(function (int $id) {
|
||||
return $this->pickupDataProvider->get($id);
|
||||
$teamerPickups = array_map(function (mixed $id) {
|
||||
return $this->pickupDataProvider->get((int) $id);
|
||||
}, $teamer->getPickups());
|
||||
|
||||
foreach ($teamerPickups as $teamerPickup) {
|
||||
if (null === $teamerPickup) {
|
||||
continue;
|
||||
}
|
||||
$teamerPickupCity = trim($teamerPickup->getCity());
|
||||
$pickupCity = trim($pickup->getCity());
|
||||
if ($teamerPickupCity === $pickupCity) {
|
||||
|
||||
@@ -80,5 +80,25 @@
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-6 first:pt-0 last:pb-0 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-bold leading-6 text-gray-900">
|
||||
erstellt
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
{{ assignment.createdAt|date('d.m.Y, H:i') }} Uhr
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-6 first:pt-0 last:pb-0 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-bold leading-6 text-gray-900">
|
||||
aktualisiert
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
{% if assignment.updatedAt %}
|
||||
{{ assignment.updatedAt|date('d.m.Y, H:i') }} Uhr
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<ul class="pb-8">
|
||||
{% if assignment.pickup %}
|
||||
{% set pickup = assignment.destination.pickup(assignment.pickup) %}
|
||||
{% if pickup is not null %}
|
||||
<li class="py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex-1">Busbegleitung ab {{ assignment.destination.pickup(assignment.pickup).city }}</span>
|
||||
<span class="flex-1">Busbegleitung ab {{ pickup.city }}</span>
|
||||
{% if has_pickup(assignment.pickup, teamer) %}
|
||||
{{ icon('check', 'w-5 h-5 text-green-500 shrink-0') }}
|
||||
{% else %}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
'endpointUpload': endpoint_upload,
|
||||
'endpointDelete': path('app_upload_delete'),
|
||||
'maxFiles': max_files|default(10),
|
||||
'maxFilesize': max_filesize|default(1),
|
||||
'maxFilesize': max_filesize|default(5),
|
||||
'acceptedFiles': accepted_files|default(null),
|
||||
'params': upload_session_params|default(null),
|
||||
'language': {
|
||||
'fileTooBig': 'uploader.file_too_big'|trans({ '%maxFilesize%': 1 }),
|
||||
'fileTooBig': 'uploader.file_too_big'|trans({ '%maxFilesize%': 5 }),
|
||||
'invalidFileType': 'uploader.invalid_file_type'|trans,
|
||||
'maxFilesExceeded': 'uploader.max_files_exceeded'|trans({ '%maxFiles%': 10 })
|
||||
}
|
||||
|
||||
@@ -171,6 +171,37 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2">
|
||||
<h2 class="font-bold text-lg pb-2">
|
||||
überfällige Feedbacks
|
||||
</h2>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for disposition in overdueFeedbacks %}
|
||||
{% set assignment = disposition.assignment %}
|
||||
<li class="py-2 first:pt-0 last:pb-0">
|
||||
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
|
||||
<span class="flex items-center space-x-1">
|
||||
{{ icon('feedback', 'w-4 h-4 shrink-0') }}
|
||||
<div class="flex items-center space-x-1 truncate">
|
||||
<span class="whitespace-nowrap">{{ disposition.teamer }}</span>
|
||||
{% if assignment.destination %}
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</span>
|
||||
{% if assignment.jobProfile %}
|
||||
<span class="pl-5 whitespace-nowrap">{{ assignment.jobProfile.name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="py-2 first:pt-0 last:pb-0">
|
||||
-
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2">
|
||||
<h2 class="font-bold text-lg pb-2">
|
||||
Überfällige Honorarverträge
|
||||
|
||||
@@ -164,13 +164,18 @@
|
||||
{{ disposition.remarks|default('-')|nl2br }}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
title="zusätzliche Absprachen {{ disposition.teamer }}"
|
||||
hx-get="{{ path('app_administrative_disposition_special_agreements', { 'uuid': disposition.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ icon('edit') }}
|
||||
</button>
|
||||
<div class="flex items-start space-x-2">
|
||||
<div class="flex-1">
|
||||
{{ disposition.specialAgreements|default('-')|nl2br }}
|
||||
</div>
|
||||
<button type="button"
|
||||
title="zusätzliche Absprachen {{ disposition.teamer }}"
|
||||
hx-get="{{ path('app_administrative_disposition_special_agreements', { 'uuid': disposition.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ icon('edit') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{{ knp_pagination_sortable(pagination, 'Jobprofil', 'job_profile.name') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ knp_pagination_sortable(pagination, 'Bus', 'assignment.pickup') }}
|
||||
{{ knp_pagination_sortable(pagination, 'Busbegleitung', 'assignment.pickup') }}
|
||||
</th>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<th>
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
{% block content %}
|
||||
{{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }}
|
||||
<div class="flex flex-col space-y-2 pb-4">
|
||||
{% if filterForm.id is defined %}
|
||||
{{ form_row(filterForm.id) }}
|
||||
{% endif %}
|
||||
{% if filterForm.status is defined %}
|
||||
{{ form_row(filterForm.status) }}
|
||||
{% endif %}
|
||||
{{ form_row(filterForm.dateFrom) }}
|
||||
{{ form_row(filterForm.dateTo) }}
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
{{ form_row(filterForm.dateFrom) }}
|
||||
{{ form_row(filterForm.dateTo) }}
|
||||
</div>
|
||||
{{ form_row(filterForm.jobProfiles) }}
|
||||
{{ form_row(filterForm.hotels) }}
|
||||
{{ form_row(filterForm.duration) }}
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
{% macro dispositionData(disposition) %}
|
||||
{% set assignment = disposition.assignment %}
|
||||
<span class="whitespace-nowrap">{{ disposition.teamer.fullName(true) }}</span>
|
||||
<button type="button"
|
||||
class="flex items-center space-x-1"
|
||||
title="Teamer:inneninfo {{ disposition.teamer }}"
|
||||
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ icon('info', 'w-4 h-4') }}
|
||||
</button>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
{% if application.comment and application.commentVisible %}
|
||||
<strong>Begründung:</strong> {{ application.comment|nl2br}}
|
||||
{% endif %}
|
||||
{% elseif application.status == constant('App\\Entity\\Application::STATUS_PENDING') %}
|
||||
{% endif %}
|
||||
{% if application.status == constant('App\\Entity\\Application::STATUS_PENDING') %}
|
||||
<twig:MessageBox message="Deine Bewerbung befindet sich in Bearbeitung."/>
|
||||
{% if application.comment and application.commentVisible %}
|
||||
<strong>Begründung:</strong> {{ application.comment|nl2br}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% if is_granted('WITHDRAW', application) %}
|
||||
<twig:MessageBox message="Du hast dich am {{ application.createdAt|date('d.m.Y') }} auf diesen Einsatz beworben."/>
|
||||
<button type="button"
|
||||
class="btn btn--secondary"
|
||||
|
||||
Reference in New Issue
Block a user