WIP: Implement application process
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
<?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 Version20231009102628 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 assignment CHANGE benefits benefits LONGTEXT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE assignment CHANGE benefits benefits JSON NOT NULL COMMENT \'(DC2Type:json)\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?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 Version20231009104620 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 assignment ADD show_available_dispositions TINYINT(1) NOT NULL, CHANGE available available_dispositions INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE assignment DROP show_available_dispositions, CHANGE available_dispositions available INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Admin\Teamer\Availability;
|
||||||
|
|
||||||
|
use App\Controller\Traits\ReturnUrlTrait;
|
||||||
|
use App\Entity\Availability;
|
||||||
|
use App\Entity\Teamer;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||||
|
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 DeleteController extends AbstractController
|
||||||
|
{
|
||||||
|
use ReturnUrlTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/teamer/availability/delete/{teamer_uuid}/{availability_id}', name: 'app_admin_teamer_availability_delete')]
|
||||||
|
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||||
|
public function index(
|
||||||
|
#[MapEntity(mapping: ['teamer_uuid' => 'uuid'])]
|
||||||
|
Teamer $teamer,
|
||||||
|
#[MapEntity(mapping: ['availability_id' => 'id'])]
|
||||||
|
Availability $availability,
|
||||||
|
Request $request
|
||||||
|
): Response {
|
||||||
|
if (null === $availability->getOwner()) {
|
||||||
|
$teamer->removeAvailability($availability);
|
||||||
|
} elseif ($teamer === $availability->getOwner()) {
|
||||||
|
$this->entityManager->remove($availability);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Der Zeitraum wurde entfernt/gelöscht.');
|
||||||
|
$this->logger->info('Delete availability', [
|
||||||
|
'teamer' => $teamer->getUuid(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$redirectUrl = $this->getReturnUrl($request, 'app_admin_teamer_profile', ['uuid' => $teamer->getUuid()]);
|
||||||
|
|
||||||
|
return $this->redirect($redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Controller\Admin\Teamer;
|
namespace App\Controller\Admin\Teamer;
|
||||||
|
|
||||||
use App\Entity\Teamer;
|
use App\Entity\Teamer;
|
||||||
|
use App\Service\Teamer\AvailabilityProcessor;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -14,8 +15,13 @@ class AvailabilityController extends AbstractController
|
|||||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||||
public function index(Teamer $teamer): Response
|
public function index(Teamer $teamer): Response
|
||||||
{
|
{
|
||||||
|
$processor = new AvailabilityProcessor();
|
||||||
|
$availabilities = $teamer->getAvailabilities()->toArray();
|
||||||
|
$groupedAvailabilities = $processor->groupRecords($availabilities);
|
||||||
|
|
||||||
return $this->render('admin/teamer/availability.html.twig', [
|
return $this->render('admin/teamer/availability.html.twig', [
|
||||||
'teamer' => $teamer,
|
'teamer' => $teamer,
|
||||||
|
'groupedAvailabilities' => $groupedAvailabilities,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Teamer;
|
||||||
|
|
||||||
|
use App\Entity\Application;
|
||||||
|
use App\Entity\Assignment;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Form\TeamerApplicationType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
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 ApplicationController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/teamer/application/{uuid}', name: 'app_teamer_application')]
|
||||||
|
#[IsGranted('ROLE_TEAMER')]
|
||||||
|
public function index(Assignment $assignment, Request $request): Response
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
$teamer = $user->getTeamer();
|
||||||
|
$application = new Application($assignment, $teamer);
|
||||||
|
|
||||||
|
$form = $this->createForm(TeamerApplicationType::class, $application);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->persist($application);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Deine Bewerbung wurde entgegengenommen');
|
||||||
|
$this->logger->info('Create application', [
|
||||||
|
'teamer' => $teamer->getUuid(),
|
||||||
|
'application' => $application->getUuid(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_teamer_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('teamer/application.html.twig', [
|
||||||
|
'assignment' => $assignment,
|
||||||
|
'teamer' => $teamer,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Teamer;
|
||||||
|
|
||||||
|
use App\Entity\Assignment;
|
||||||
|
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 AssignmentController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/teamer/assignment/{uuid}', name: 'app_teamer_assignment')]
|
||||||
|
#[IsGranted('ROLE_TEAMER')]
|
||||||
|
public function index(Assignment $assignment): Response
|
||||||
|
{
|
||||||
|
return $this->render('teamer/assignment.html.twig', [
|
||||||
|
'assignment' => $assignment,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@
|
|||||||
namespace App\Controller\Teamer;
|
namespace App\Controller\Teamer;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Repository\AssignmentRepository;
|
||||||
|
use Knp\Component\Pager\PaginatorInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
@@ -11,12 +14,16 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
|||||||
|
|
||||||
class IndexController extends AbstractController
|
class IndexController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ValidatorInterface $validator)
|
public function __construct(
|
||||||
{}
|
private readonly ValidatorInterface $validator,
|
||||||
|
private readonly PaginatorInterface $paginator,
|
||||||
|
private readonly AssignmentRepository $assignmentRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/teamer', name: 'app_teamer_index')]
|
#[Route('/teamer', name: 'app_teamer_index')]
|
||||||
#[IsGranted('ROLE_TEAMER')]
|
#[IsGranted('ROLE_TEAMER')]
|
||||||
public function index(): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
@@ -25,7 +32,23 @@ class IndexController extends AbstractController
|
|||||||
// Validate teamer data to show missing data right away
|
// Validate teamer data to show missing data right away
|
||||||
$errors = $this->validator->validate($teamer, null, ['profile_preflight']);
|
$errors = $this->validator->validate($teamer, null, ['profile_preflight']);
|
||||||
|
|
||||||
|
$query = $this
|
||||||
|
->assignmentRepository
|
||||||
|
->getListQuery()
|
||||||
|
;
|
||||||
|
|
||||||
|
$pagination = $this->paginator->paginate(
|
||||||
|
$query,
|
||||||
|
$request->query->getInt('page', 1),
|
||||||
|
10,
|
||||||
|
[
|
||||||
|
'defaultSortFieldName' => 'assignment.dateFrom',
|
||||||
|
'defaultSortDirection' => 'asc',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return $this->render('teamer/index.html.twig', [
|
return $this->render('teamer/index.html.twig', [
|
||||||
|
'pagination' => $pagination,
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Controller\Teamer\Profile\Availability;
|
namespace App\Controller\Teamer\Profile\Availability;
|
||||||
|
|
||||||
use App\Entity\Availability;
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Repository\AvailabilityRepository;
|
use App\Repository\AvailabilityRepository;
|
||||||
use Carbon\Carbon;
|
use App\Service\Teamer\AvailabilityProcessor;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -24,39 +23,17 @@ class IndexController extends AbstractController
|
|||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
$teamer = $user->getTeamer();
|
$teamer = $user->getTeamer();
|
||||||
|
|
||||||
|
$processor = new AvailabilityProcessor();
|
||||||
|
|
||||||
$availabilities = $teamer->getAvailabilities()->toArray();
|
$availabilities = $teamer->getAvailabilities()->toArray();
|
||||||
$selectedAvailabilities = $this->groupRecords($availabilities);
|
$selectedAvailabilities = $processor->groupRecords($availabilities);
|
||||||
|
|
||||||
$availabilities = $this->availabilityRepository->getSelectableForTeamer($teamer);
|
$availabilities = $this->availabilityRepository->getSelectableForTeamer($teamer);
|
||||||
$selectableAvailabilities = $this->groupRecords($availabilities);
|
$selectableAvailabilities = $processor->groupRecords($availabilities);
|
||||||
|
|
||||||
return $this->render('teamer/profile/availability/index.html.twig', [
|
return $this->render('teamer/profile/availability/index.html.twig', [
|
||||||
'selectedAvailabilities' => $selectedAvailabilities,
|
'selectedAvailabilities' => $selectedAvailabilities,
|
||||||
'selectableAvailabilities' => $selectableAvailabilities,
|
'selectableAvailabilities' => $selectableAvailabilities,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function groupRecords(array $records): array
|
|
||||||
{
|
|
||||||
$selectableAvailabilities = [];
|
|
||||||
|
|
||||||
foreach ($records as $record) {
|
|
||||||
/** @var Availability $record */
|
|
||||||
$dateFrom = $record->getDateFrom();
|
|
||||||
$year = $dateFrom->format('Y');
|
|
||||||
$month = (new Carbon($dateFrom))->locale('de_DE')->monthName;
|
|
||||||
|
|
||||||
if (false === isset($selectableAvailabilities[$year])) {
|
|
||||||
$selectableAvailabilities[$year] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (false === isset($selectableAvailabilities[$year][$month])) {
|
|
||||||
$selectableAvailabilities[$year][$month] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$selectableAvailabilities[$year][$month][] = $record;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $selectableAvailabilities;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Traits;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
trait ReturnUrlTrait
|
||||||
|
{
|
||||||
|
public function getReturnUrl(Request $request, string $defaultRoute, array $parameters = []): string
|
||||||
|
{
|
||||||
|
$defaultUrl = $this->generateUrl($defaultRoute, $parameters);
|
||||||
|
|
||||||
|
return rawurldecode($request->query->get('r', $defaultUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,37 +6,57 @@ use App\Entity\Traits\TimestampableEntity;
|
|||||||
use App\Repository\ApplicationRepository;
|
use App\Repository\ApplicationRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: ApplicationRepository::class)]
|
#[ORM\Entity(repositoryClass: ApplicationRepository::class)]
|
||||||
class Application implements TimestampableEntityInterface
|
class Application implements TimestampableEntityInterface
|
||||||
{
|
{
|
||||||
use TimestampableEntity;
|
use TimestampableEntity;
|
||||||
|
|
||||||
|
public const STATUS_NEW = 'new';
|
||||||
|
public const STATUS_PENDING = 'pending';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'applications')]
|
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||||
private ?Assignment $assignment = null;
|
private string $uuid;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'applications')]
|
#[ORM\ManyToOne(inversedBy: 'applications')]
|
||||||
private ?Teamer $teamer = null;
|
private ?Assignment $assignment;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'applications')]
|
||||||
|
private ?Teamer $teamer;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private ?string $requests = null;
|
private ?string $requests = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 64)]
|
#[ORM\Column(length: 64)]
|
||||||
private ?string $status = null;
|
private ?string $status;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private ?string $remarks = null;
|
private ?string $remarks = null;
|
||||||
|
|
||||||
|
public function __construct(Assignment $assignment, Teamer $teamer)
|
||||||
|
{
|
||||||
|
$this->uuid = Uuid::v4();
|
||||||
|
$this->assignment = $assignment;
|
||||||
|
$this->teamer = $teamer;
|
||||||
|
$this->status = static::STATUS_NEW;
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUuid(): string
|
||||||
|
{
|
||||||
|
return $this->uuid;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAssignment(): ?Assignment
|
public function getAssignment(): ?Assignment
|
||||||
{
|
{
|
||||||
return $this->assignment;
|
return $this->assignment;
|
||||||
|
|||||||
+25
-11
@@ -4,7 +4,6 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use App\Entity\Traits\BlameableEntity;
|
use App\Entity\Traits\BlameableEntity;
|
||||||
use App\Entity\Traits\TimestampableEntity;
|
use App\Entity\Traits\TimestampableEntity;
|
||||||
use App\Model\BenefitsDto;
|
|
||||||
use App\Repository\AssignmentRepository;
|
use App\Repository\AssignmentRepository;
|
||||||
use Carbon\CarbonPeriod;
|
use Carbon\CarbonPeriod;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -38,7 +37,10 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
|||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
#[Assert\Range(min: 1, minMessage: 'Die Mindestanzahl ist 1')]
|
#[Assert\Range(min: 1, minMessage: 'Die Mindestanzahl ist 1')]
|
||||||
private ?int $available = 1;
|
private ?int $availableDispositions = 1;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?bool $showAvailableDispositions = false;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||||
private ?\DateTimeImmutable $dateFrom = null;
|
private ?\DateTimeImmutable $dateFrom = null;
|
||||||
@@ -53,8 +55,8 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?int $pickup = null;
|
private ?int $pickup = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private array $benefits;
|
private ?string $benefits;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private ?string $remarks = null;
|
private ?string $remarks = null;
|
||||||
@@ -78,7 +80,7 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
|||||||
$this->fees = new ArrayCollection();
|
$this->fees = new ArrayCollection();
|
||||||
$this->applications = new ArrayCollection();
|
$this->applications = new ArrayCollection();
|
||||||
$this->dispositions = new ArrayCollection();
|
$this->dispositions = new ArrayCollection();
|
||||||
$this->benefits = BenefitsDto::$defaults;
|
$this->benefits = "Anreise im E&P Reisebus\nUnterkunft\nVerpflegung\nSkipass\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -115,14 +117,26 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAvailable(): ?int
|
public function getAvailableDispositions(): ?int
|
||||||
{
|
{
|
||||||
return $this->available;
|
return $this->availableDispositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAvailable(?int $available): static
|
public function setAvailableDispositions(?int $availableDispositions): static
|
||||||
{
|
{
|
||||||
$this->available = $available;
|
$this->availableDispositions = $availableDispositions;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isShowAvailableDispositions(): ?bool
|
||||||
|
{
|
||||||
|
return $this->showAvailableDispositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShowAvailableDispositions(bool $showAvailableDispositions): static
|
||||||
|
{
|
||||||
|
$this->showAvailableDispositions = $showAvailableDispositions;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -184,12 +198,12 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBenefits(): array
|
public function getBenefits(): ?string
|
||||||
{
|
{
|
||||||
return $this->benefits;
|
return $this->benefits;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBenefits(array $benefits): static
|
public function setBenefits(?string $benefits): static
|
||||||
{
|
{
|
||||||
$this->benefits = $benefits;
|
$this->benefits = $benefits;
|
||||||
|
|
||||||
|
|||||||
@@ -692,6 +692,11 @@ class Teamer implements TimestampableEntityInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasPickup(int $pickupId): bool
|
||||||
|
{
|
||||||
|
return in_array($pickupId, $this->getPickups());
|
||||||
|
}
|
||||||
|
|
||||||
public function getLanguage(): ?string
|
public function getLanguage(): ?string
|
||||||
{
|
{
|
||||||
return $this->language;
|
return $this->language;
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ use App\Entity\Destination;
|
|||||||
use App\Entity\Fee;
|
use App\Entity\Fee;
|
||||||
use App\Entity\JobProfile;
|
use App\Entity\JobProfile;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Model\BenefitsDto;
|
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@@ -22,8 +21,12 @@ class AssignmentType extends AbstractType
|
|||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('available', IntegerType::class, [
|
->add('availableDispositions', IntegerType::class, [
|
||||||
'label' => 'Anzahl',
|
'label' => 'zu vergeben',
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
->add('showAvailableDispositions', CheckboxType::class, [
|
||||||
|
'label' => 'Anzahl anzeigen',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('destination', AutocompleteEntityType::class, [
|
->add('destination', AutocompleteEntityType::class, [
|
||||||
@@ -56,12 +59,12 @@ class AssignmentType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'placeholder' => 'Keine Busbegleitung',
|
'placeholder' => 'Keine Busbegleitung',
|
||||||
])
|
])
|
||||||
->add('benefits', ChoiceType::class, [
|
->add('benefits', TextareaType::class, [
|
||||||
'label' => 'Benefits',
|
'label' => 'Benefits',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'multiple' => true,
|
'attr' => [
|
||||||
'expanded' => true,
|
'rows' => 5,
|
||||||
'choices' => array_flip(BenefitsDto::$values),
|
],
|
||||||
])
|
])
|
||||||
->add('fees', EntityType::class, [
|
->add('fees', EntityType::class, [
|
||||||
'label' => 'Honorar(e)',
|
'label' => 'Honorar(e)',
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\Application;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||||
|
|
||||||
|
class TeamerApplicationType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
||||||
|
/** @var Application $application */
|
||||||
|
$application = $event->getData();
|
||||||
|
|
||||||
|
if (null === $application) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignment = $application->getAssignment();
|
||||||
|
|
||||||
|
if (null === $assignment->getPickup()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$teamer = $application->getTeamer();
|
||||||
|
|
||||||
|
if (in_array($assignment->getPickup(), $teamer->getPickups())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->getForm()->add('confirmPickup', CheckboxType::class, [
|
||||||
|
'label' => 'Ich bestätige den abweichenden Buszustieg',
|
||||||
|
'mapped' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new IsTrue([
|
||||||
|
'message' => 'Deine Bestätigung ist erforderlich',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Application::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,14 @@ class TeamerMenuBuilder extends AbstractMenuBuilder
|
|||||||
'route' => 'app_teamer_index',
|
'route' => 'app_teamer_index',
|
||||||
'title' => 'Einsatzübersicht',
|
'title' => 'Einsatzübersicht',
|
||||||
'icon' => 'calendar',
|
'icon' => 'calendar',
|
||||||
|
'hideChildren' => true,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'route' => 'app_teamer_assignment',
|
||||||
|
'title' => 'Einsatzdetails',
|
||||||
|
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
|
||||||
|
]
|
||||||
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'route' => false,
|
'route' => false,
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Model;
|
|
||||||
|
|
||||||
class BenefitsDto
|
|
||||||
{
|
|
||||||
public static array $defaults = [
|
|
||||||
'anreise',
|
|
||||||
'unterkunft',
|
|
||||||
'verpflegung',
|
|
||||||
'skipass',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static array $values = [
|
|
||||||
'anreise' => 'Anreise im E&P Reisebus',
|
|
||||||
'unterkunft' => 'Unterkunft',
|
|
||||||
'verpflegung' => 'Verpflegung',
|
|
||||||
'skipass' => 'Skipass',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Teamer;
|
||||||
|
|
||||||
|
use App\Entity\Availability;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class AvailabilityProcessor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Groups provided unsorted availabilities by year and month
|
||||||
|
* [
|
||||||
|
* '2023' => [
|
||||||
|
* 'Januar' => [
|
||||||
|
* ...
|
||||||
|
* ],
|
||||||
|
* 'Juni' => [
|
||||||
|
* ...
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
public function groupRecords(array $records): array
|
||||||
|
{
|
||||||
|
$availabilities = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
/** @var Availability $record */
|
||||||
|
$dateFrom = $record->getDateFrom();
|
||||||
|
$year = $dateFrom->format('Y');
|
||||||
|
$month = (new Carbon($dateFrom))->locale('de_DE')->monthName;
|
||||||
|
|
||||||
|
if (false === isset($availabilities[$year])) {
|
||||||
|
$availabilities[$year] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === isset($availabilities[$year][$month])) {
|
||||||
|
$availabilities[$year][$month] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$availabilities[$year][$month][] = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $availabilities;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ class AppExtension extends AbstractExtension
|
|||||||
public function getFilters(): array
|
public function getFilters(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
new TwigFilter('nl2list', [AppRuntime::class, 'nl2List'], ['is_safe' => ['html']]),
|
||||||
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
|
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
|
||||||
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
|
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
|
||||||
new TwigFilter('date_diff', [AppRuntime::class, 'dateDiffForHumans']),
|
new TwigFilter('date_diff', [AppRuntime::class, 'dateDiffForHumans']),
|
||||||
@@ -19,7 +20,6 @@ class AppExtension extends AbstractExtension
|
|||||||
new TwigFilter('teamer_status_label', [AppRuntime::class, 'teamerStatusLabel']),
|
new TwigFilter('teamer_status_label', [AppRuntime::class, 'teamerStatusLabel']),
|
||||||
new TwigFilter('bpn_country_label', [AppRuntime::class, 'bpnCountryLabel']),
|
new TwigFilter('bpn_country_label', [AppRuntime::class, 'bpnCountryLabel']),
|
||||||
new TwigFilter('bpn_pickup_label', [AppRuntime::class, 'bpnPickupLabel']),
|
new TwigFilter('bpn_pickup_label', [AppRuntime::class, 'bpnPickupLabel']),
|
||||||
new TwigFilter('bpn_destination_label', [AppRuntime::class, 'bpnDestinationLabel']),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ class AppExtension extends AbstractExtension
|
|||||||
return [
|
return [
|
||||||
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
|
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
|
||||||
new TwigFunction('is_current_route', [AppRuntime::class, 'isCurrentRoute']),
|
new TwigFunction('is_current_route', [AppRuntime::class, 'isCurrentRoute']),
|
||||||
|
new TwigFunction('return_url', [AppRuntime::class, 'getEncodedReturnUrl']),
|
||||||
new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]),
|
new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-20
@@ -3,9 +3,7 @@
|
|||||||
namespace App\Twig;
|
namespace App\Twig;
|
||||||
|
|
||||||
use App\BusProNet\DataProvider\CountryDataProvider;
|
use App\BusProNet\DataProvider\CountryDataProvider;
|
||||||
use App\BusProNet\DataProvider\HotelDataProvider;
|
|
||||||
use App\BusProNet\DataProvider\PickupDataProvider;
|
use App\BusProNet\DataProvider\PickupDataProvider;
|
||||||
use App\BusProNet\Model\Country;
|
|
||||||
use App\Entity\Teamer;
|
use App\Entity\Teamer;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
@@ -16,12 +14,11 @@ use Twig\Extra\Intl\IntlExtension;
|
|||||||
class AppRuntime implements RuntimeExtensionInterface
|
class AppRuntime implements RuntimeExtensionInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RequestStack $requestStack,
|
private readonly RequestStack $requestStack,
|
||||||
private readonly IntlExtension $intlExtension,
|
private readonly IntlExtension $intlExtension,
|
||||||
private readonly CountryDataProvider $countries,
|
private readonly CountryDataProvider $countries,
|
||||||
private readonly PickupDataProvider $pickups,
|
private readonly PickupDataProvider $pickups,
|
||||||
private readonly HotelDataProvider $hotels,
|
private readonly string $environment
|
||||||
private readonly string $environment
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +44,7 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
return 'unbekannt';
|
return 'unbekannt';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'nationality' === $property ? $country->getNationality() : $country->getName();
|
return 'nationality' === $property ? $country->getNationality() : $country->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bpnPickupLabel(int $id): string
|
public function bpnPickupLabel(int $id): string
|
||||||
@@ -61,17 +58,6 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
return $pickup->getCity();
|
return $pickup->getCity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bpnDestinationLabel(int $id): string
|
|
||||||
{
|
|
||||||
$hotel = $this->hotels->get($id);
|
|
||||||
|
|
||||||
if (null === $hotel) {
|
|
||||||
return 'unbekannt';
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf('%s, %s', $hotel->getName(), $hotel->getCity() ?? '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fileIconFilter(Environment $environment, string $mimeType, ?string $classes = 'w-4 h-4'): string
|
public function fileIconFilter(Environment $environment, string $mimeType, ?string $classes = 'w-4 h-4'): string
|
||||||
{
|
{
|
||||||
$icon = match ($mimeType) {
|
$icon = match ($mimeType) {
|
||||||
@@ -89,7 +75,7 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
$factor = floor((strlen($bytes) - 1) / 3);
|
$factor = floor((strlen($bytes) - 1) / 3);
|
||||||
|
|
||||||
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)).@$size[$factor];
|
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)) . @$size[$factor];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function formatMoney(int $amount): string
|
public function formatMoney(int $amount): string
|
||||||
@@ -108,6 +94,19 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function nl2List(string $content, string $class = 'list-disc pl-4'): string
|
||||||
|
{
|
||||||
|
$items = explode(PHP_EOL, $content);
|
||||||
|
|
||||||
|
$list = '<ul class="'.$class.'">';
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$list .= '<li>'.$item.'</li>';
|
||||||
|
}
|
||||||
|
$list .= '<ul>';
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
public function isCurrentRoute(string $route): bool
|
public function isCurrentRoute(string $route): bool
|
||||||
{
|
{
|
||||||
$requestedRoute = $this->requestStack->getMainRequest()->attributes->get('_route');
|
$requestedRoute = $this->requestStack->getMainRequest()->attributes->get('_route');
|
||||||
@@ -115,6 +114,17 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
return $requestedRoute === $route;
|
return $requestedRoute === $route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEncodedReturnUrl(): string
|
||||||
|
{
|
||||||
|
$masterRequest = $this->requestStack->getMainRequest();
|
||||||
|
|
||||||
|
if (null === $masterRequest) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawurlencode($masterRequest->getRequestUri());
|
||||||
|
}
|
||||||
|
|
||||||
public function renderQaAttribute(string $label, string $value = null): string
|
public function renderQaAttribute(string $label, string $value = null): string
|
||||||
{
|
{
|
||||||
if ('test' !== $this->environment) {
|
if ('test' !== $this->environment) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
<div class="flex flex-col space-y-4 pb-8">
|
<div class="flex flex-col space-y-4 pb-8">
|
||||||
<div class="grid lg:grid-cols-4 gap-x-8 gap-y-4">
|
<div class="grid lg:grid-cols-5 gap-x-8 gap-y-4">
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
{{ form_row(form.destination) }}
|
{{ form_row(form.destination) }}
|
||||||
</div>
|
</div>
|
||||||
{{ form_row(form.available) }}
|
{{ form_row(form.availableDispositions) }}
|
||||||
|
{{ form_row(form.showAvailableDispositions) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
|
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
|
||||||
{{ form_row(form.dateFrom) }}
|
{{ form_row(form.dateFrom) }}
|
||||||
|
|||||||
@@ -11,6 +11,45 @@
|
|||||||
<h1 class="text-2xl font-bold pb-8">
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
Verfügbarkeit {{ teamer }}
|
Verfügbarkeit {{ teamer }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="grid md:grid-cols-2 gap-8 pb-8">
|
||||||
|
{% for year, months in groupedAvailabilities %}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold">
|
||||||
|
{{ year }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-col space-y-3">
|
||||||
|
{% for month, availabilities in months %}
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">
|
||||||
|
{{ month }}
|
||||||
|
</h4>
|
||||||
|
{% for availability in availabilities %}
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>{{ availability }}</span>
|
||||||
|
{% if is_granted('DELETE', availability) %}
|
||||||
|
<button type="button"
|
||||||
|
title="Zeitraum entfernen/löschen"
|
||||||
|
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
|
||||||
|
{{ stimulus_action('modal-button', 'confirmation', null, {
|
||||||
|
'title': 'Bist du sicher?',
|
||||||
|
'content': 'Möchtest du den Zeitraum wirklich löschen?',
|
||||||
|
'target-url': path('app_admin_teamer_availability_delete', { 'teamer_uuid': teamer.uuid, 'availability_id': availability.id, 'r': return_url() })
|
||||||
|
}) }}>
|
||||||
|
{{ icon('delete', 'w-5 h-5') }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="md:col-span-2 text-sm">
|
||||||
|
{{ teamer.firstName }} hat bisher keine Verfügbarkeiten hinterlegt.
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-bold pb-2">
|
<h2 class="text-lg font-bold pb-2">
|
||||||
Mögliche Jobprofile
|
Mögliche Jobprofile
|
||||||
@@ -85,6 +85,12 @@
|
|||||||
<li>
|
<li>
|
||||||
{{ profile.name }}
|
{{ profile.name }}
|
||||||
</li>
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<div class="text-sm">
|
||||||
|
{{ teamer.firstName }} hat bisher keine Job-Profile ausgewählt.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,42 +1,44 @@
|
|||||||
<div class="bg-gray-100 border border-gray-300 border-t-0 px-4 py-3 flex items-center justify-between sm:px-4">
|
{% if pageCount > 0 %}
|
||||||
<div class="flex-1 flex items-center justify-between">
|
<div class="bg-gray-100 border border-gray-300 border-t-0 px-4 py-3 flex items-center justify-between sm:px-4">
|
||||||
<div>
|
<div class="flex-1 flex items-center justify-between">
|
||||||
<p class="text-sm leading-5 text-gray-700">{{ 'paginator.info'|trans({ '{from}': firstItemNumber, '{to}': lastItemNumber, '{total}': totalCount })|raw }}</p>
|
<div>
|
||||||
</div>
|
<p class="text-sm leading-5 text-gray-700">{{ 'paginator.info'|trans({ '{from}': firstItemNumber, '{to}': lastItemNumber, '{total}': totalCount })|raw }}</p>
|
||||||
<div>
|
</div>
|
||||||
<nav class="relative z-0 inline-flex">
|
<div>
|
||||||
{% if pageCount > 1 %}
|
<nav class="relative z-0 inline-flex">
|
||||||
{% if previous is defined %}
|
{% if pageCount > 1 %}
|
||||||
<a href="{{ path(route, query|merge({(pageParameterName): previous})) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium hover:bg-secondary hover:text-white" aria-label="Previous">
|
{% if previous is defined %}
|
||||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<a href="{{ path(route, query|merge({(pageParameterName): previous})) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium hover:bg-secondary hover:text-white" aria-label="Previous">
|
||||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
</svg>
|
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
</a>
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if current > 3 %}
|
||||||
|
<span class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% for page in pagesInRange %}
|
||||||
|
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium focus:z-10 hover:bg-secondary {% if page == current %}bg-primary text-white hover:text-white{% else %}bg-white text-gray-700 hover:text-white{% endif %}">
|
||||||
|
{{ page }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% if current < pageCount - 3 %}
|
||||||
|
<span class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if next is defined %}
|
||||||
|
<a href="{{ path(route, query|merge({(pageParameterName): next})) }}" class="-ml-px relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium hover:bg-secondary hover:text-white" aria-label="Next">
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current > 3 %}
|
</nav>
|
||||||
<span class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700">
|
</div>
|
||||||
...
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% for page in pagesInRange %}
|
|
||||||
<a href="{{ path(route, query|merge({(pageParameterName): page})) }}" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium focus:z-10 hover:bg-secondary {% if page == current %}bg-primary text-white hover:text-white{% else %}bg-white text-gray-700 hover:text-white{% endif %}">
|
|
||||||
{{ page }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% if current < pageCount - 3 %}
|
|
||||||
<span class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700">
|
|
||||||
...
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if next is defined %}
|
|
||||||
<a href="{{ path(route, query|merge({(pageParameterName): next})) }}" class="-ml-px relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium hover:bg-secondary hover:text-white" aria-label="Next">
|
|
||||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{% extends 'teamer/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Bewerbung{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
|
Bewerbung
|
||||||
|
</h1>
|
||||||
|
<div class="grid grid-cols-2 gap-8 items-start">
|
||||||
|
<div class="grid grid-cols-2 gap-x-4 gap-y-3 bg-gray-100 rounded-md p-4">
|
||||||
|
<div class="font-bold text-xl">
|
||||||
|
Jobprofil
|
||||||
|
</div>
|
||||||
|
<div class="font-bold text-xl">
|
||||||
|
{{ assignment.jobProfile.name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Destination
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.destination.product }}
|
||||||
|
<br>
|
||||||
|
{{ assignment.destination.hotel }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Einsatztermin
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.totalPeriod.start|date('d.m.Y') }} - {{ assignment.totalPeriod.end|date('d.m.Y') }}
|
||||||
|
</div>
|
||||||
|
{% if assignment.pickup and assignment.pickupDate %}
|
||||||
|
<div>
|
||||||
|
Busabfahrt
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.pickupDate|date('d.m.Y') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Busbegleitung
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
ab {{ assignment.pickup|bpn_pickup_label }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
Du bekommst
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.benefits|nl2list }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Honorar
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% for fee in assignment.fees %}
|
||||||
|
{{ fee.name }} {{ fee.value|format_money }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold pb-2">
|
||||||
|
Voraussetzungen für diesen Einsatz
|
||||||
|
</h2>
|
||||||
|
<ul class="list-disc pl-4 pb-4">
|
||||||
|
{% if assignment.pickup and assignment.pickupDate %}
|
||||||
|
<li>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>Busbegleitung ab {{ assignment.pickup|bpn_pickup_label }} am {{ assignment.pickupDate|date('d.m.Y') }}</span>
|
||||||
|
{% if teamer.hasPickup(assignment.pickup) %}
|
||||||
|
{{ icon('check', 'w-5 h-5 text-emerald-500') }}
|
||||||
|
{% else %}
|
||||||
|
{{ icon('close', 'w-5 h-5 text-red-500') }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for training in assignment.jobProfile.requiredTrainings %}
|
||||||
|
<li>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>{{ training.name }} absolviert</span>
|
||||||
|
{% if teamer.trainingAttendance(training) %}
|
||||||
|
{{ icon('check', 'w-5 h-5 text-emerald-500') }}
|
||||||
|
{% else %}
|
||||||
|
{{ icon('close', 'w-5 h-5 text-red-500') }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{% if form.confirmPickup is defined %}
|
||||||
|
{{ form_row(form.confirmPickup) }}
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="btn">
|
||||||
|
Bewerbung einreichen
|
||||||
|
</button>
|
||||||
|
{{ form_rest(form) }}
|
||||||
|
{{ form_end(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{% extends 'teamer/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Einsatzübersicht{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
|
Einsatzübersicht
|
||||||
|
</h1>
|
||||||
|
<div class="grid grid-cols-2 gap-8 items-start">
|
||||||
|
<div class="grid grid-cols-2 gap-x-4 gap-y-3 bg-gray-100 rounded-md p-4">
|
||||||
|
<div class="font-bold text-xl">
|
||||||
|
Jobprofil
|
||||||
|
</div>
|
||||||
|
<div class="font-bold text-xl">
|
||||||
|
{{ assignment.jobProfile.name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Destination
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.destination.product }}
|
||||||
|
<br>
|
||||||
|
{{ assignment.destination.hotel }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Einsatztermin
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.totalPeriod.start|date('d.m.Y') }} - {{ assignment.totalPeriod.end|date('d.m.Y') }}
|
||||||
|
</div>
|
||||||
|
{% if assignment.pickup and assignment.pickupDate %}
|
||||||
|
<div>
|
||||||
|
Busabfahrt
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.pickupDate|date('d.m.Y') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Busbegleitung
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
ab {{ assignment.pickup|bpn_pickup_label }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
Du bekommst
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ assignment.benefits|nl2list }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Honorar
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% for fee in assignment.fees %}
|
||||||
|
{{ fee.name }} {{ fee.value|format_money }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold pb-2">
|
||||||
|
Voraussetzungen für diesen Einsatz
|
||||||
|
</h2>
|
||||||
|
<ul class="list-disc pl-4 pb-4">
|
||||||
|
{% if assignment.pickup and assignment.pickupDate %}
|
||||||
|
<li>
|
||||||
|
Busbegleitung ab {{ assignment.pickup|bpn_pickup_label }} am {{ assignment.pickupDate|date('d.m.Y') }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for training in assignment.jobProfile.requiredTrainings %}
|
||||||
|
<li>
|
||||||
|
{{ training.name }} absolviert
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="pb-4">
|
||||||
|
Erfüllst du mehrere oder alle Voraussetzungen für diesen Einsatz?
|
||||||
|
</p>
|
||||||
|
<a href="{{ path('app_teamer_application', { 'uuid': assignment.uuid }) }}" class="btn">
|
||||||
|
Hier bewerben!
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
{% extends 'teamer/layout.html.twig' %}
|
{% extends 'teamer/layout.html.twig' %}
|
||||||
|
|
||||||
{% block title %}Mein Dashboard{% endblock %}
|
{% block title %}Einsatzübersicht{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
|
Einsatzübersicht
|
||||||
|
</h1>
|
||||||
{% if errors|length %}
|
{% if errors|length %}
|
||||||
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-8">
|
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-8">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -14,4 +17,76 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="data-table-wrapper">
|
||||||
|
<div class="data-table-wrapper__inner">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{{ knp_pagination_sortable(pagination, 'Einsatz­beginn', 'assignment.dateFrom') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ knp_pagination_sortable(pagination, 'Einsatz­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 }) }}">
|
||||||
|
{{ 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>
|
||||||
|
<a href="{{ path('app_admin_assignment_create') }}" class="btn">
|
||||||
|
Neu
|
||||||
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user