WIP: Implement application process

This commit is contained in:
Björn Fromme
2023-10-09 14:42:37 +02:00
parent 8e3499581a
commit abced8c924
26 changed files with 802 additions and 138 deletions
+31
View File
@@ -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)\'');
}
}
+31
View File
@@ -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;
use App\Entity\Teamer;
use App\Service\Teamer\AvailabilityProcessor;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -14,8 +15,13 @@ class AvailabilityController extends AbstractController
#[IsGranted('ROLE_ADMINISTRATIVE')]
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', [
'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,
]);
}
}
+26 -3
View File
@@ -3,7 +3,10 @@
namespace App\Controller\Teamer;
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;
@@ -11,12 +14,16 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
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')]
#[IsGranted('ROLE_TEAMER')]
public function index(): Response
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
@@ -25,7 +32,23 @@ class IndexController extends AbstractController
// Validate teamer data to show missing data right away
$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', [
'pagination' => $pagination,
'errors' => $errors,
]);
}
@@ -2,10 +2,9 @@
namespace App\Controller\Teamer\Profile\Availability;
use App\Entity\Availability;
use App\Entity\User;
use App\Repository\AvailabilityRepository;
use Carbon\Carbon;
use App\Service\Teamer\AvailabilityProcessor;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -24,39 +23,17 @@ class IndexController extends AbstractController
$user = $this->getUser();
$teamer = $user->getTeamer();
$processor = new AvailabilityProcessor();
$availabilities = $teamer->getAvailabilities()->toArray();
$selectedAvailabilities = $this->groupRecords($availabilities);
$selectedAvailabilities = $processor->groupRecords($availabilities);
$availabilities = $this->availabilityRepository->getSelectableForTeamer($teamer);
$selectableAvailabilities = $this->groupRecords($availabilities);
$selectableAvailabilities = $processor->groupRecords($availabilities);
return $this->render('teamer/profile/availability/index.html.twig', [
'selectedAvailabilities' => $selectedAvailabilities,
'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;
}
}
+15
View File
@@ -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));
}
}
+24 -4
View File
@@ -6,37 +6,57 @@ use App\Entity\Traits\TimestampableEntity;
use App\Repository\ApplicationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: ApplicationRepository::class)]
class Application implements TimestampableEntityInterface
{
use TimestampableEntity;
public const STATUS_NEW = 'new';
public const STATUS_PENDING = 'pending';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'applications')]
private ?Assignment $assignment = null;
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[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)]
private ?string $requests = null;
#[ORM\Column(length: 64)]
private ?string $status = null;
private ?string $status;
#[ORM\Column(type: Types::TEXT, nullable: true)]
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
{
return $this->id;
}
public function getUuid(): string
{
return $this->uuid;
}
public function getAssignment(): ?Assignment
{
return $this->assignment;
+25 -11
View File
@@ -4,7 +4,6 @@ namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Model\BenefitsDto;
use App\Repository\AssignmentRepository;
use Carbon\CarbonPeriod;
use Doctrine\Common\Collections\ArrayCollection;
@@ -38,7 +37,10 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
#[ORM\Column(nullable: true)]
#[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)]
private ?\DateTimeImmutable $dateFrom = null;
@@ -53,8 +55,8 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
#[ORM\Column(nullable: true)]
private ?int $pickup = null;
#[ORM\Column]
private array $benefits;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $benefits;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $remarks = null;
@@ -78,7 +80,7 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
$this->fees = new ArrayCollection();
$this->applications = 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
@@ -115,14 +117,26 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
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;
}
@@ -184,12 +198,12 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
return $this;
}
public function getBenefits(): array
public function getBenefits(): ?string
{
return $this->benefits;
}
public function setBenefits(array $benefits): static
public function setBenefits(?string $benefits): static
{
$this->benefits = $benefits;
+5
View File
@@ -692,6 +692,11 @@ class Teamer implements TimestampableEntityInterface
return $this;
}
public function hasPickup(int $pickupId): bool
{
return in_array($pickupId, $this->getPickups());
}
public function getLanguage(): ?string
{
return $this->language;
+11 -8
View File
@@ -7,11 +7,10 @@ use App\Entity\Destination;
use App\Entity\Fee;
use App\Entity\JobProfile;
use App\Entity\User;
use App\Model\BenefitsDto;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
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\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -22,8 +21,12 @@ class AssignmentType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('available', IntegerType::class, [
'label' => 'Anzahl',
->add('availableDispositions', IntegerType::class, [
'label' => 'zu vergeben',
'required' => false,
])
->add('showAvailableDispositions', CheckboxType::class, [
'label' => 'Anzahl anzeigen',
'required' => false,
])
->add('destination', AutocompleteEntityType::class, [
@@ -56,12 +59,12 @@ class AssignmentType extends AbstractType
'required' => false,
'placeholder' => 'Keine Busbegleitung',
])
->add('benefits', ChoiceType::class, [
->add('benefits', TextareaType::class, [
'label' => 'Benefits',
'required' => false,
'multiple' => true,
'expanded' => true,
'choices' => array_flip(BenefitsDto::$values),
'attr' => [
'rows' => 5,
],
])
->add('fees', EntityType::class, [
'label' => 'Honorar(e)',
+56
View File
@@ -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,
]);
}
}
+8
View File
@@ -13,6 +13,14 @@ class TeamerMenuBuilder extends AbstractMenuBuilder
'route' => 'app_teamer_index',
'title' => 'Einsatzübersicht',
'icon' => 'calendar',
'hideChildren' => true,
'children' => [
[
'route' => 'app_teamer_assignment',
'title' => 'Einsatzdetails',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
]
],
],
[
'route' => false,
-20
View File
@@ -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;
}
}
+2 -1
View File
@@ -11,6 +11,7 @@ class AppExtension extends AbstractExtension
public function getFilters(): array
{
return [
new TwigFilter('nl2list', [AppRuntime::class, 'nl2List'], ['is_safe' => ['html']]),
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
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('bpn_country_label', [AppRuntime::class, 'bpnCountryLabel']),
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 [
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
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']]),
];
}
+24 -14
View File
@@ -3,9 +3,7 @@
namespace App\Twig;
use App\BusProNet\DataProvider\CountryDataProvider;
use App\BusProNet\DataProvider\HotelDataProvider;
use App\BusProNet\DataProvider\PickupDataProvider;
use App\BusProNet\Model\Country;
use App\Entity\Teamer;
use Carbon\Carbon;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -20,7 +18,6 @@ class AppRuntime implements RuntimeExtensionInterface
private readonly IntlExtension $intlExtension,
private readonly CountryDataProvider $countries,
private readonly PickupDataProvider $pickups,
private readonly HotelDataProvider $hotels,
private readonly string $environment
) {
}
@@ -61,17 +58,6 @@ class AppRuntime implements RuntimeExtensionInterface
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
{
$icon = match ($mimeType) {
@@ -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
{
$requestedRoute = $this->requestStack->getMainRequest()->attributes->get('_route');
@@ -115,6 +114,17 @@ class AppRuntime implements RuntimeExtensionInterface
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
{
if ('test' !== $this->environment) {
+3 -2
View File
@@ -1,10 +1,11 @@
{{ form_start(form) }}
<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">
{{ form_row(form.destination) }}
</div>
{{ form_row(form.available) }}
{{ form_row(form.availableDispositions) }}
{{ form_row(form.showAvailableDispositions) }}
</div>
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
{{ form_row(form.dateFrom) }}
@@ -11,6 +11,45 @@
<h1 class="text-2xl font-bold pb-8">
Verfügbarkeit {{ teamer }}
</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>
{% endblock %}
+7 -1
View File
@@ -75,7 +75,7 @@
{% endfor %}
</ul>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-8">
<div>
<h2 class="text-lg font-bold pb-2">
Mögliche Jobprofile
@@ -85,6 +85,12 @@
<li>
{{ profile.name }}
</li>
{% else %}
<li>
<div class="text-sm">
{{ teamer.firstName }} hat bisher keine Job-Profile ausgewählt.
</div>
</li>
{% endfor %}
</ul>
</div>
+2
View File
@@ -1,3 +1,4 @@
{% if pageCount > 0 %}
<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 class="flex-1 flex items-center justify-between">
<div>
@@ -40,3 +41,4 @@
</div>
</div>
</div>
{% endif %}
+101
View File
@@ -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 %}
+84
View File
@@ -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 %}
+76 -1
View File
@@ -1,8 +1,11 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Mein Dashboard{% endblock %}
{% block title %}Einsatzübersicht{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Einsatzübersicht
</h1>
{% if errors|length %}
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-8">
<ul>
@@ -14,4 +17,76 @@
</ul>
</div>
{% endif %}
<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 }) }}">
{{ 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 %}