WIP: Implement application process

This commit is contained in:
Björn Fromme
2023-10-10 17:17:30 +02:00
parent 3a458df94f
commit 6910ab6134
34 changed files with 937 additions and 119 deletions
+8
View File
@@ -3,6 +3,12 @@
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"repositories": [
{
"type": "composer",
"url": "https://www.setasign.com/downloads/"
}
],
"require": {
"php": ">=8.1",
"ext-ctype": "*",
@@ -18,6 +24,8 @@
"oneup/uploader-bundle": "^4.0",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.22",
"setasign/fpdf": "^1.8",
"setasign/fpdi": "^2.5",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.3.*",
"symfony/console": "6.3.*",
Generated
+119 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ffce78078f5185643c9ddfa6e2146e49",
"content-hash": "69e7597627d8e6c883d063d873a08d78",
"packages": [
{
"name": "doctrine/cache",
@@ -2671,6 +2671,124 @@
},
"time": "2021-07-14T16:46:02+00:00"
},
{
"name": "setasign/fpdf",
"version": "1.8.6",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDF.git",
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-zlib": "*"
},
"type": "library",
"autoload": {
"classmap": [
"fpdf.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Olivier Plathey",
"email": "[email protected]",
"homepage": "http://fpdf.org/"
}
],
"description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.",
"homepage": "http://www.fpdf.org",
"keywords": [
"fpdf",
"pdf"
],
"support": {
"source": "https://github.com/Setasign/FPDF/tree/1.8.6"
},
"time": "2023-06-26T14:44:25+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "ecf0459643ec963febfb9a5d529dcd93656006a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/ecf0459643ec963febfb9a5d529dcd93656006a4",
"reference": "ecf0459643ec963febfb9a5d529dcd93656006a4",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^5.6 || ^7.0 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "~5.7",
"setasign/fpdf": "~1.8",
"setasign/tfpdf": "~1.31",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "~6.2"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "[email protected]",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "[email protected]",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.5.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2023-09-28T10:46:27+00:00"
},
{
"name": "symfony/apache-pack",
"version": "v1.0.1",
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231010125452 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE disposition ADD contract_pdf_id INT DEFAULT NULL, ADD invoice_pdf_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE disposition ADD CONSTRAINT FK_4C58BF606D5F2C5F FOREIGN KEY (contract_pdf_id) REFERENCES upload (id)');
$this->addSql('ALTER TABLE disposition ADD CONSTRAINT FK_4C58BF60EA295441 FOREIGN KEY (invoice_pdf_id) REFERENCES upload (id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_4C58BF606D5F2C5F ON disposition (contract_pdf_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_4C58BF60EA295441 ON disposition (invoice_pdf_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE disposition DROP FOREIGN KEY FK_4C58BF606D5F2C5F');
$this->addSql('ALTER TABLE disposition DROP FOREIGN KEY FK_4C58BF60EA295441');
$this->addSql('DROP INDEX UNIQ_4C58BF606D5F2C5F ON disposition');
$this->addSql('DROP INDEX UNIQ_4C58BF60EA295441 ON disposition');
$this->addSql('ALTER TABLE disposition DROP contract_pdf_id, DROP invoice_pdf_id');
}
}
@@ -2,17 +2,43 @@
namespace App\Controller\Admin\Application;
use App\Repository\ApplicationRepository;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(
private readonly ApplicationRepository $applicationRepository,
private readonly PaginatorInterface $paginator
) {
}
#[Route('/admin/application', name: 'app_admin_application_index')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(): Response
public function index(Request $request): Response
{
return $this->render('admin/application/index.html.twig');
$query = $this
->applicationRepository
->getListQuery()
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
'defaultSortFieldName' => 'destination.dateFrom',
'defaultSortDirection' => 'asc',
]
);
return $this->render('admin/application/index.html.twig', [
'pagination' => $pagination,
]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Controller\Admin\DIsposition;
use App\Entity\Application;
use App\Entity\Disposition;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class CreateController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/disposition/create/{uuid}', name: 'app_admin_disposition_create')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Application $application): Response
{
$disposition = new Disposition($application);
$this->entityManager->persist($disposition);
$this->entityManager->remove($application);
$this->entityManager->flush();
$this->addFlash('success', 'Der Teamer wurde eingeteilt');
$this->logger->info('Create disposition', [
'disposition' => $disposition->getUuid(),
]);
return $this->redirectToRoute('app_admin_assignment_detail', [
'uuid' => $application->getAssignment()->getUuid(),
]);
}
}
+25 -5
View File
@@ -2,22 +2,30 @@
namespace App\Controller\Teamer;
use App\Controller\Traits\ReturnUrlTrait;
use App\Entity\Assignment;
use App\Entity\User;
use App\Repository\ApplicationRepository;
use App\Repository\DispositionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class AssignmentController extends AbstractController
{
public function __construct(private readonly ApplicationRepository $applicationRepository)
{}
use ReturnUrlTrait;
public function __construct(
private readonly ApplicationRepository $applicationRepository,
private readonly DispositionRepository $dispositionRepository
) {
}
#[Route('/teamer/assignment/{uuid}', name: 'app_teamer_assignment')]
#[IsGranted('ROLE_TEAMER')]
public function index(Assignment $assignment): Response
public function index(Assignment $assignment, Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
@@ -31,11 +39,23 @@ class AssignmentController extends AbstractController
])
;
return $this->render('teamer/assignment.html.twig', [
$disposition = $this
->dispositionRepository
->findOneBy([
'assignment' => $assignment,
'teamer' => $teamer,
])
;
$isBookmarked = $teamer->getBookmarks()->contains($assignment);
return $this->render('teamer/assignment/index.html.twig', [
'teamer' => $teamer,
'assignment' => $assignment,
'disposition' => $disposition,
'application' => $application,
'isBookmarked' => $teamer->getBookmarks()->contains($assignment),
'isBookmarked' => $isBookmarked,
'returnUrl' => $this->getReturnUrl($request, 'app_teamer_index'),
]);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Controller\Teamer\Bookmark;
use App\Entity\User;
use App\Repository\AssignmentRepository;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(
private readonly AssignmentRepository $assignmentRepository,
private readonly PaginatorInterface $paginator
) {
}
#[Route('/teamer/bookmark', name: 'app_teamer_bookmark_index')]
#[IsGranted('ROLE_TEAMER')]
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$query = $this
->assignmentRepository
->getBookmarkQueryForTeamer($teamer)
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
'defaultSortFieldName' => 'assignment.dateFrom',
'defaultSortDirection' => 'asc',
]
);
return $this->render('teamer/bookmark/index.html.twig', [
'pagination' => $pagination,
]);
}
}
+1 -1
View File
@@ -11,6 +11,6 @@ class ContactController extends AbstractController
#[Route('/teamer/contact', name: 'app_teamer_contact')]
public function index(): Response
{
return $this->render('teamer/contact.html.twig');
return $this->render('teamer/contact/index.html.twig');
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Controller\Teamer\Disposition;
use App\Entity\Disposition;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DetailController extends AbstractController
{
#[Route('/teamer/disposition/detail/{uuid}', name: 'app_teamer_disposition_detail')]
#[IsGranted('VIEW', subject: 'disposition')]
public function index(Disposition $disposition): Response
{
return $this->render('teamer/disposition/detail.html.twig', [
'disposition' => $disposition,
]);
}
}
@@ -1,18 +0,0 @@
<?php
namespace App\Controller\Teamer\Disposition;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
#[Route('/teamer/disposition', name: 'app_teamer_disposition_index')]
#[IsGranted('ROLE_TEAMER')]
public function index(): Response
{
return $this->render('');
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Controller\Teamer\Disposition;
use App\Entity\User;
use App\Repository\DispositionRepository;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class RecentController extends AbstractController
{
public function __construct(
private readonly DispositionRepository $dispositionRepository,
private readonly PaginatorInterface $paginator
) {
}
#[Route('/teamer/disposition/recent', name: 'app_teamer_disposition_recent')]
#[IsGranted('ROLE_TEAMER')]
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$query = $this
->dispositionRepository
->getRecentQuery($teamer)
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
'defaultSortFieldName' => 'assignment.dateFrom',
'defaultSortDirection' => 'asc',
]
);
return $this->render('teamer/disposition/recent.html.twig', [
'pagination' => $pagination,
]);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Controller\Teamer\Disposition;
use App\Entity\User;
use App\Repository\DispositionRepository;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class UpcomingController extends AbstractController
{
public function __construct(
private readonly DispositionRepository $dispositionRepository,
private readonly PaginatorInterface $paginator
) {
}
#[Route('/teamer/disposition/upcoming', name: 'app_teamer_disposition_upcoming')]
#[IsGranted('ROLE_TEAMER')]
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$query = $this
->dispositionRepository
->getUpcomingQuery($teamer)
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
'defaultSortFieldName' => 'assignment.dateFrom',
'defaultSortDirection' => 'asc',
]
);
return $this->render('teamer/disposition/upcoming.html.twig', [
'pagination' => $pagination,
]);
}
}
@@ -1,18 +0,0 @@
<?php
namespace App\Controller\Teamer\Disposition;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class WatchlistController extends AbstractController
{
#[Route('/teamer/disposition/watchlist', name: 'app_teamer_disposition_watchlist')]
#[IsGranted('ROLE_TEAMER')]
public function index(): Response
{
return $this->render('');
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ class FaqController extends AbstractController
{
$faqs = $this->faqRepository->findBy([], ['sorting' => 'ASC']);
return $this->render('teamer/faq.html.twig', [
return $this->render('teamer/faq/index.html.twig', [
'faqs' => $faqs,
]);
}
+1
View File
@@ -15,6 +15,7 @@ class Application implements TimestampableEntityInterface
public const STATUS_NEW = 'new';
public const STATUS_PENDING = 'pending';
public const STATUS_REJECTED = 'rejected';
#[ORM\Id]
#[ORM\GeneratedValue]
+28
View File
@@ -73,12 +73,16 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
#[ORM\ManyToOne]
private ?User $contact = null;
#[ORM\ManyToMany(targetEntity: Teamer::class, mappedBy: 'bookmarks')]
private Collection $teamers;
public function __construct()
{
$this->uuid = Uuid::v4();
$this->fees = new ArrayCollection();
$this->applications = new ArrayCollection();
$this->dispositions = new ArrayCollection();
$this->teamers = new ArrayCollection();
$this->benefits = "Anreise im E&P Reisebus\nUnterkunft\nVerpflegung\nSkipass\n";
}
@@ -316,4 +320,28 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
return $this;
}
/**
* @return Collection<int, Teamer>
*/
public function getTeamers(): Collection
{
return $this->teamers;
}
public function addTeamer(Teamer $teamer): static
{
if (!$this->teamers->contains($teamer)) {
$this->teamers->add($teamer);
}
return $this;
}
public function removeTeamer(Teamer $teamer): static
{
$this->teamers->removeElement($teamer);
return $this;
}
}
+37 -4
View File
@@ -24,17 +24,26 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
private string $uuid;
#[ORM\ManyToOne(inversedBy: 'dispositions')]
private ?Assignment $assignment = null;
private ?Assignment $assignment;
#[ORM\ManyToOne(inversedBy: 'dispositions')]
private ?Teamer $teamer = null;
private ?Teamer $teamer;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $remarks = null;
private ?string $remarks;
public function __construct()
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
private ?Upload $contractPdf = null;
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
private ?Upload $invoicePdf = null;
public function __construct(Application $application)
{
$this->uuid = Uuid::v4();
$this->assignment = $application->getAssignment();
$this->teamer = $application->getTeamer();
$this->remarks = $application->getRemarks();
}
public function getId(): ?int
@@ -82,4 +91,28 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
return $this;
}
public function getContractPdf(): ?Upload
{
return $this->contractPdf;
}
public function setContractPdf(?Upload $contractPdf): static
{
$this->contractPdf = $contractPdf;
return $this;
}
public function getInvoicePdf(): ?Upload
{
return $this->invoicePdf;
}
public function setInvoicePdf(?Upload $invoicePdf): static
{
$this->invoicePdf = $invoicePdf;
return $this;
}
}
+7 -7
View File
@@ -94,25 +94,25 @@ class Teamer implements TimestampableEntityInterface
#[Assert\NotNull(message: 'Bitte lade ein Foto von dir hoch', groups: ['profile', 'profile_preflight'])]
private ?Upload $photo = null;
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Feedback::class)]
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Feedback::class, cascade: ['remove'])]
private Collection $feedback;
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: TrainingAttendance::class)]
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: TrainingAttendance::class, cascade: ['remove'])]
private Collection $trainingAttendances;
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: License::class)]
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: License::class, cascade: ['remove'])]
private Collection $licenses;
#[ORM\ManyToMany(targetEntity: JobProfile::class)]
#[ORM\ManyToMany(targetEntity: JobProfile::class, orphanRemoval: true)]
private Collection $jobProfiles;
#[ORM\ManyToMany(targetEntity: Assignment::class)]
#[ORM\ManyToMany(targetEntity: Assignment::class, inversedBy: 'teamers', orphanRemoval: true)]
private Collection $bookmarks;
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Application::class)]
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Application::class, cascade: ['remove'])]
private Collection $applications;
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Disposition::class)]
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Disposition::class, cascade: ['remove'])]
private Collection $dispositions;
#[ORM\OneToOne(mappedBy: 'teamer')]
+6 -6
View File
@@ -47,27 +47,27 @@ class TeamerMenuBuilder extends AbstractMenuBuilder
'icon' => 'bus',
'children' => [
[
'route' => 'app_teamer_disposition_watchlist',
'route' => 'app_teamer_bookmark_index',
'title' => 'Merkliste',
],
[
'route' => 'app_teamer_disposition_watchlist',
'route' => 'app_teamer_bookmark_index',
'title' => 'Offene Bewerbungen',
],
[
'route' => 'app_teamer_disposition_watchlist',
'route' => 'app_teamer_disposition_upcoming',
'title' => 'Nächste Einsätze',
],
[
'route' => 'app_teamer_disposition_watchlist',
'route' => 'app_teamer_disposition_recent',
'title' => 'Letzte Einsätze',
],
[
'route' => 'app_teamer_disposition_watchlist',
'route' => 'app_teamer_bookmark_index',
'title' => 'Meine Dokumente',
],
[
'route' => 'app_teamer_disposition_watchlist',
'route' => 'app_teamer_bookmark_index',
'title' => 'Mein Feedback',
],
],
+15
View File
@@ -4,6 +4,7 @@ namespace App\Repository;
use App\Entity\Application;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\Persistence\ManagerRegistry;
/**
@@ -20,4 +21,18 @@ class ApplicationRepository extends ServiceEntityRepository
{
parent::__construct($registry, Application::class);
}
public function getListQuery(): Query
{
$qb = $this->createQueryBuilder('application');
return $qb
->select('application', 'assignment', 'destination', 'job_profile', 'teamer')
->innerJoin('application.assignment', 'assignment')
->innerJoin('assignment.destination', 'destination')
->innerJoin('assignment.jobProfile', 'job_profile')
->innerJoin('application.teamer', 'teamer')
->getQuery()
;
}
}
+16
View File
@@ -43,4 +43,20 @@ class AssignmentRepository extends ServiceEntityRepository
return $qb->getQuery();
}
public function getBookmarkQueryForTeamer(Teamer $teamer): Query
{
$qb = $this->createQueryBuilder('assignment');
$qb
->select('assignment', 'destination', 'job_profile')
->innerJoin('assignment.teamers', 'teamer', 'WITH', 'teamer = :teamer')
->innerJoin('assignment.destination', 'destination')
->innerJoin('assignment.jobProfile', 'job_profile')
->leftJoin('assignment.applications', 'application')
->setParameter('teamer', $teamer)
;
return $qb->getQuery();
}
}
+50 -35
View File
@@ -3,7 +3,9 @@
namespace App\Repository;
use App\Entity\Disposition;
use App\Entity\Teamer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\Persistence\ManagerRegistry;
/**
@@ -21,46 +23,59 @@ class DispositionRepository extends ServiceEntityRepository
parent::__construct($registry, Disposition::class);
}
public function save(Disposition $entity, bool $flush = false): void
public function getUpcomingQuery(Teamer $teamer): Query
{
$this->getEntityManager()->persist($entity);
$qb = $this->createQueryBuilder('disposition');
if ($flush) {
$this->getEntityManager()->flush();
}
return $qb
->select('disposition', 'assignment', 'job_profile', 'destination')
->innerJoin('disposition.assignment', 'assignment')
->innerJoin('assignment.jobProfile', 'job_profile')
->innerJoin('assignment.destination', 'destination')
->where($qb->expr()->andX(
$qb->expr()->eq('disposition.teamer', ':teamer'),
$qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->isNull('assignment.dateFrom'),
$qb->expr()->gte('destination.dateFrom', ':date')
),
$qb->expr()->andX(
$qb->expr()->isNotNull('assignment.dateFrom'),
$qb->expr()->gte('assignment.dateFrom', ':date')
)
)
))
->setParameter('teamer', $teamer)
->setParameter('date', new \DateTimeImmutable())
->getQuery()
;
}
public function remove(Disposition $entity, bool $flush = false): void
public function getRecentQuery(Teamer $teamer): Query
{
$this->getEntityManager()->remove($entity);
$qb = $this->createQueryBuilder('disposition');
if ($flush) {
$this->getEntityManager()->flush();
return $qb
->select('disposition', 'assignment', 'job_profile', 'destination')
->innerJoin('disposition.assignment', 'assignment')
->innerJoin('assignment.jobProfile', 'job_profile')
->innerJoin('assignment.destination', 'destination')
->where($qb->expr()->andX(
$qb->expr()->eq('disposition.teamer', ':teamer'),
$qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->isNull('assignment.dateTo'),
$qb->expr()->lte('destination.dateTo', ':date')
),
$qb->expr()->andX(
$qb->expr()->isNotNull('assignment.dateTo'),
$qb->expr()->lte('assignment.dateTo', ':date')
)
)
))
->setParameter('teamer', $teamer)
->setParameter('date', new \DateTimeImmutable())
->getQuery()
;
}
}
// /**
// * @return Disposition[] Returns an array of Disposition objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('d')
// ->andWhere('d.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('d.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Disposition
// {
// return $this->createQueryBuilder('d')
// ->andWhere('d.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Security\Voter;
use App\Entity\Disposition;
use App\Entity\User;
use App\Repository\ApplicationRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class DispositionVoter extends Voter
{
public const VIEW = 'VIEW';
public const EDIT = 'EDIT';
public const DELETE = 'DELETE';
public function __construct(private readonly ApplicationRepository $applicationRepository)
{}
protected function supports(string $attribute, mixed $subject): bool
{
if (!$subject instanceof Disposition) {
return false;
}
return in_array($attribute, [static::VIEW, static::EDIT, static::DELETE]);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
// Administrative users have full access to all dispositions
if (in_array('ROLE_ADMINISTRATIVE', $token->getRoleNames())) {
return true;
}
// Teamers may only view or edit their own dispositions
if (in_array('ROLE_TEAMER', $token->getRoleNames()) && in_array($attribute, [static::VIEW, static::EDIT])) {
/** @var User $user */
$user = $token->getUser();
$teamer = $user->getTeamer();
/** @var Disposition $disposition */
$disposition = $subject;
return $teamer === $disposition->getTeamer();
}
return false;
}
}
@@ -11,9 +11,64 @@
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatzbeginn', 'destination.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatzende', 'destination.dateTo') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Jobprofil', 'job_profile.name') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Destination', 'destination.product') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Busbegleitung ab', 'destination.pickup') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Bewerber*in', 'teamer.lastName') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for application in pagination %}
{% set assignment = application.assignment %}
<tr>
<td>
{{ assignment.destination.dateFrom|date('d.m.Y') }}
</td>
<td>
{{ assignment.destination.dateTo|date('d.m.Y') }}
</td>
<td>
{{ assignment.jobProfile.name }}
</td>
<td>
{{ assignment.destination.product }}
<br>
{{ assignment.destination.hotel }}
</td>
<td>
{{ assignment.pickup ? assignment.pickup|bpn_pickup_label : '-' }}
</td>
<td>
{{ application.teamer }}
</td>
<td>
</td>
</tr>
{% else %}
<tr>
<td colspan="7">
<div class="text-center">
Keine Daten
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@@ -6,4 +6,27 @@
<h1 class="text-2xl font-bold pb-8">
Einsatzübersicht
</h1>
<div class="grid grid-cols-2 gap-8 items-start">
<div class="bg-gray-100 rounded-md p-4">
{% include '_partials/_assignment_info.html.twig' %}
</div>
<div>
{% for application in applications %}
<a href="{{ path('app_admin_teamer_profile', { 'uuid': application.teamer.uuid, 'r': return_url() }) }}">
{{ application.teamer }}
</a>
{{ application.requests }}
<button type="button"
class="btn"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du den Teamer ' ~ application.teamer ~ ' wirklich einteilen?',
'target-url': path('app_admin_disposition_create', { 'uuid': application.uuid })
}) }}>
einteilen
</button>
{% endfor %}
</div>
</div>
{% endblock %}
@@ -15,20 +15,15 @@
Voraussetzungen für diesen Einsatz
</h2>
{% include '_partials/_assignment_requirements_info.html.twig' %}
{% if application is null %}
<div class="pb-4">
Erfüllst du mehrere oder alle Voraussetzungen für diesen Einsatz?
<div class="flex flex-col items-start space-y-4">
{% if disposition is not null %}
<div>
Du wurdest am {{ disposition.createdAt|date('d.m.Y') }} für diesen Einsatz eingeteilt.
</div>
<div class="pb-4">
<a href="{{ path('app_teamer_application_create', { 'uuid': assignment.uuid }) }}" class="btn">
Hier bewerben!
</a>
</div>
{% else %}
<div class="pb-4">
{% elseif application is not null %}
<div>
Du hast dich am {{ application.createdAt|date('d.m.Y') }} auf diesen Einsatz beworben.
</div>
<div class="pb-4">
<button type="button"
class="btn btn--secondary"
title="Bewerbung zurückziehen"
@@ -40,13 +35,23 @@
}) }}>
Bewerbung zurückziehen
</button>
{% else %}
<div>
Erfüllst du mehrere oder alle Voraussetzungen für diesen Einsatz?
</div>
<a href="{{ path('app_teamer_application_create', { 'uuid': assignment.uuid }) }}" class="btn">
Hier bewerben!
</a>
{% endif %}
{% if not isBookmarked %}
<a href="{{ path('app_teamer_bookmark_toggle', { 'uuid': assignment.uuid, 'r': return_url() }) }}" class="underline">
auf die Merkliste setzen
</a>
{% endif %}
<a href="{{ returnUrl }}" class="underline">
zurück
</a>
</div>
</div>
</div>
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Meine Merkliste{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Meine Merkliste
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatz&shy;beginn', 'assignment.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatz&shy;ende', 'assignment.dateTo') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Jobprofil', 'assignment.jobProfile') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Destination', 'destination.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Busbegleitung ab', 'assignment.pickup') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for assignment in pagination %}
<tr>
<td>
{% set dateFrom = assignment.dateFrom ? assignment.dateFrom : assignment.destination.dateFrom %}
{{ dateFrom|date('d.m.Y') }}
</td>
<td>
{% set dateTo = assignment.dateTo ? assignment.dateTo : assignment.destination.dateTo %}
{{ dateTo|date('d.m.Y') }}
</td>
<td>
{{ assignment.jobProfile.name }}
</td>
<td>
{{ assignment.destination.product }}
<br>
{{ assignment.destination.hotel }}
</td>
<td>
{{ assignment.pickup ? assignment.pickup|bpn_pickup_label|default('-') : '-' }}
<br>
{{ assignment.pickupDate ? assignment.pickupDate|date('d.m.Y') : '' }}
</td>
<td>
<div class="flex items-center space-x-1 justify-end">
<a href="{{ path('app_teamer_assignment', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
{{ icon('info', 'w-5 h-5') }}
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6">
<div class="text-center">
Keine Daten
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endblock %}
@@ -0,0 +1,70 @@
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatz&shy;beginn', 'assignment.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatz&shy;ende', 'assignment.dateTo') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Jobprofil', 'assignment.jobProfile') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Destination', 'destination.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Busbegleitung ab', 'assignment.pickup') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for disposition in pagination %}
{% set assignment = disposition.assignment %}
<tr>
<td>
{% set dateFrom = assignment.dateFrom ? assignment.dateFrom : assignment.destination.dateFrom %}
{{ dateFrom|date('d.m.Y') }}
</td>
<td>
{% set dateTo = assignment.dateTo ? assignment.dateTo : assignment.destination.dateTo %}
{{ dateTo|date('d.m.Y') }}
</td>
<td>
{{ assignment.jobProfile.name }}
</td>
<td>
{{ assignment.destination.product }}
<br>
{{ assignment.destination.hotel }}
</td>
<td>
{{ assignment.pickup ? assignment.pickup|bpn_pickup_label|default('-') : '-' }}
<br>
{{ assignment.pickupDate ? assignment.pickupDate|date('d.m.Y') : '' }}
</td>
<td>
<div class="flex items-center space-x-1 justify-end">
<a href="{{ path('app_teamer_disposition_detail', { 'uuid': disposition.uuid, 'r': return_url() }) }}">
{{ icon('info', 'w-5 h-5') }}
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6">
<div class="text-center">
Keine Daten
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
@@ -0,0 +1,25 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Einsatzdetails{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Einsatzdetails
</h1>
<div class="grid grid-cols-2 gap-8 items-start">
<div class="bg-gray-100 rounded-md p-4">
{% set assignment = disposition.assignment %}
{% include '_partials/_assignment_info.html.twig' %}
</div>
<div>
<h2 class="text-2xl font-bold pb-8">
Dein Einsatz wurde bestätigt!
</h2>
<p class="pb-4">
Dein Ansprechpartner für alle reisebezogenen Themen ist:
<br>
{{ assignment.contact }}, <a href="mailto:{{ assignment.contact.email }}" class="underline">{{ assignment.contact.email }}</a>
</p>
</div>
</div>
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Einsatzübersicht{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Meine letzten Einsätze
</h1>
{% include 'teamer/disposition/_table.html.twig' %}
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Einsatzübersicht{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Meine nächsten Einsätze
</h1>
{% include 'teamer/disposition/_table.html.twig' %}
{% endblock %}
+1 -1
View File
@@ -66,7 +66,7 @@
</td>
<td>
<div class="flex items-center space-x-1 justify-end">
<a href="{{ path('app_teamer_assignment', { 'uuid': assignment.uuid }) }}">
<a href="{{ path('app_teamer_assignment', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
{{ icon('info', 'w-5 h-5') }}
</a>
</div>