Feat: Implement availabilities

This commit is contained in:
Björn Fromme
2023-10-03 17:42:10 +02:00
parent cc9700a558
commit 25a016a02b
35 changed files with 827 additions and 422 deletions
+9 -3
View File
@@ -141,9 +141,15 @@
<symbol id="icon-folder" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-bus" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!-- Font Awesome Pro 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) -->
<path d="M488 128h-8V80c0-44.8-99.2-80-224-80S32 35.2 32 80v48h-8c-13.25 0-24 10.74-24 24v80c0 13.25 10.75 24 24 24h8v160c0 17.67 14.33 32 32 32v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h192v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h6.4c16 0 25.6-12.8 25.6-25.6V256h8c13.25 0 24-10.75 24-24v-80c0-13.26-10.75-24-24-24zM112 400c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32zm16-112c-17.67 0-32-14.33-32-32V128c0-17.67 14.33-32 32-32h256c17.67 0 32 14.33 32 32v128c0 17.67-14.33 32-32 32H128zm272 112c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32z"/>
<symbol id="icon-bus" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24" stroke="currentColor" fill="none">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M18 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 17h-2v-11a1 1 0 0 1 1 -1h14a5 7 0 0 1 5 7v5h-2m-4 0h-8" />
<path d="M16 5l1.5 7l4.5 0" />
<path d="M2 10l15 0" />
<path d="M7 5l0 5" />
<path d="M12 5l0 5" />
</symbol>
<symbol id="icon-ski" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1,55 @@
<?php
namespace App\Controller\Admin\System\Availability;
use App\Entity\Availability;
use App\Form\AvailabilityType;
use App\Model\AjaxModalResponseDto;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
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/system/availability/create', name: 'app_admin_system_availability_create')]
#[IsGranted('ROLE_ADMIN')]
public function index(Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$action = $this->generateUrl('app_admin_system_availability_create');
$availability = new Availability();
$form = $this->createForm(AvailabilityType::class, $availability, ['action' => $action, 'ajax_submit' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($availability);
$this->entityManager->flush();
$this->addFlash('success', 'Der Zeitraum wurde hinzugefügt.');
$this->logger->info('Add availability', [
'availability' => $availability,
]);
$redirectUrl = $this->generateUrl('app_admin_system_availability_index');
$response->setCloseAndRedirect($redirectUrl);
} else {
$content = $this->renderView('admin/system/availability/create.html.twig', [
'form' => $form,
]);
$response->setContent($content);
}
return $this->json($response);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Controller\Admin\System\Availability;
use App\Entity\Availability;
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 DeleteController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/availability/delete/{id}', name: 'app_admin_system_availability_delete', methods: ['POST'])]
#[IsGranted('ROLE_ADMIN')]
public function index(Availability $availability): Response
{
$this->entityManager->remove($availability);
$this->entityManager->flush();
$this->addFlash('success', 'Die Verfügbarkeit wurde gelöscht');
$this->logger->info('Delete availability', [
'availability' => $availability,
]);
return $this->redirectToRoute('app_admin_system_availability_index');
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Controller\Admin\System\Availability;
use App\Repository\AvailabilityRepository;
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 AvailabilityRepository $availabilityRepository,
private readonly PaginatorInterface $paginator
) {
}
#[Route('/admin/system/availability', name: 'app_admin_system_availability_index')]
#[IsGranted('ROLE_ADMIN')]
public function index(Request $request): Response
{
$query = $this
->availabilityRepository
->getListQuery()
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
'defaultSortFieldName' => 'availability.dateFrom',
'defaultSortDirection' => 'asc',
]
);
return $this->render('admin/system/availability/index.html.twig', [
'pagination' => $pagination,
]);
}
}
@@ -4,11 +4,12 @@ namespace App\Controller\Admin\System\Fee;
use App\Entity\Fee;
use App\Form\FeeType;
use App\Model\AjaxModalResponseDto;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -22,10 +23,13 @@ class CreateController extends AbstractController
#[Route('/admin/system/fee/create', name: 'app_admin_system_fee_create')]
#[IsGranted('ROLE_ADMIN')]
public function index(Request $request): Response
public function index(Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$action = $this->generateUrl('app_admin_system_fee_create');
$fee = new Fee();
$form = $this->createForm(FeeType::class, $fee);
$form = $this->createForm(FeeType::class, $fee, ['action' => $action, 'ajax_submit' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
@@ -37,11 +41,15 @@ class CreateController extends AbstractController
'fee' => $fee->getName(),
]);
return $this->redirectToRoute('app_admin_system_fee_index');
$redirectUrl = $this->generateUrl('app_admin_system_fee_index');
$response->setCloseAndRedirect($redirectUrl);
} else {
$content = $this->renderView('admin/system/fee/create.html.twig', [
'form' => $form
]);
$response->setContent($content);
}
return $this->render('admin/system/fee/create.html.twig', [
'form' => $form
]);
return $this->json($response);
}
}
@@ -1,46 +0,0 @@
<?php
namespace App\Controller\Admin\System\Fee;
use App\Entity\Fee;
use App\Form\FeeType;
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 EditController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/fee/edit/{id}', name: 'app_admin_system_fee_edit')]
#[IsGranted('ROLE_ADMIN')]
public function index(Fee $fee, Request $request): Response
{
$form = $this->createForm(FeeType::class, $fee);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($fee);
$this->entityManager->flush();
$this->addFlash('success', 'Das Honorar wurde aktualisiert');
$this->logger->info('Edit fee', [
'fee' => $fee->getName(),
]);
return $this->redirectToRoute('app_admin_system_fee_index');
}
return $this->render('admin/system/fee/edit.html.twig', [
'form' => $form
]);
}
}
@@ -4,11 +4,12 @@ namespace App\Controller\Admin\System\Training;
use App\Entity\Training;
use App\Form\TrainingType;
use App\Model\AjaxModalResponseDto;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -22,10 +23,13 @@ class CreateController extends AbstractController
#[Route('/admin/system/training/create', name: 'app_admin_system_training_create')]
#[IsGranted('ROLE_ADMIN')]
public function index(Request $request): Response
public function index(Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$action = $this->generateUrl('app_admin_system_training_create');
$training = new Training();
$form = $this->createForm(TrainingType::class, $training);
$form = $this->createForm(TrainingType::class, $training, ['action' => $action, 'ajax_submit' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
@@ -37,11 +41,15 @@ class CreateController extends AbstractController
'training' => $training->getName(),
]);
return $this->redirectToRoute('app_admin_system_training_index');
$redirectUrl = $this->generateUrl('app_admin_system_training_index');
$response->setCloseAndRedirect($redirectUrl);
} else {
$content = $this->renderView('admin/system/training/create.html.twig', [
'form' => $form
]);
$response->setContent($content);
}
return $this->render('admin/system/training/create.html.twig', [
'form' => $form
]);
return $this->json($response);
}
}
@@ -1,45 +0,0 @@
<?php
namespace App\Controller\Admin\System\Training;
use App\Entity\Training;
use App\Form\TrainingType;
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 EditController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/training/edit/{id}', name: 'app_admin_system_training_edit')]
#[IsGranted('ROLE_ADMIN')]
public function index(Training $training, Request $request): Response
{
$form = $this->createForm(TrainingType::class, $training);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush();
$this->addFlash('success', 'Die Fortbildung wurde aktualisiert');
$this->logger->info('Edit training', [
'training' => $training->getName(),
]);
return $this->redirectToRoute('app_admin_system_training_index');
}
return $this->render('admin/system/training/edit.html.twig', [
'form' => $form
]);
}
}
@@ -1,60 +0,0 @@
<?php
namespace App\Controller\Teamer\Profile;
use App\Entity\Availability;
use App\Entity\User;
use App\Repository\AvailabilityRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
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 AvailabilitiesController extends AbstractController
{
public function __construct(
private readonly AvailabilityRepository $availabilityRepository,
private readonly EntityManagerInterface $entityManager
) {
}
#[Route('/teamer/profile/availabilities', name: 'app_teamer_profile_availabilities')]
#[IsGranted('ROLE_TEAMER')]
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$selectableAvailabilities = $this->availabilityRepository->getSelectable($teamer);
$form = $this->createFormBuilder($teamer)
->add('availabilities', EntityType::class, [
'label' => false,
'class' => Availability::class,
'choice_label' => function($choice) {
return sprintf('%s - %s', $choice->getDateFrom()->format('d.m.Y'), $choice->getDateTo()->format('d.m.Y'));
},
'choices' => $selectableAvailabilities,
'multiple' => true,
'expanded' => true,
])
->getForm()
;
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush();
return $this->redirectToRoute('app_teamer_profile_availabilities');
}
return $this->render('teamer/profile/availabilities.html.twig', [
'availabilities' => $teamer->getAvailabilities(),
'selectableAvailabilities' => $selectableAvailabilities,
'form' => $form,
]);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Controller\Teamer\Profile\Availability;
use App\Entity\Availability;
use App\Entity\User;
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 AddController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/teamer/profile/availability/add/{id}', name: 'app_teamer_profile_availability_add')]
#[IsGranted('ROLE_TEAMER')]
public function index(Availability $availability): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$teamer->addAvailability($availability);
$this->entityManager->flush();
$this->logger->info('Add availability', [
'user' => $user->getUserIdentifier(),
'availability' => $availability,
]);
return $this->redirectToRoute('app_teamer_profile_availability_index');
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Controller\Teamer\Profile\Availability;
use App\Entity\Availability;
use App\Entity\User;
use App\Form\AvailabilityType;
use App\Model\AjaxModalResponseDto;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
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('/teamer/profile/availability/create', name: 'app_teamer_profile_availability_create')]
#[IsGranted('ROLE_TEAMER')]
public function index(Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$action = $this->generateUrl('app_teamer_profile_availability_create');
$availability = new Availability();
$form = $this->createForm(AvailabilityType::class, $availability, ['action' => $action, 'ajax_submit' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$teamer
->addAvailability($availability)
->addCustomAvailability($availability)
;
$this->entityManager->persist($availability);
$this->entityManager->flush();
$this->addFlash('success', 'Der Zeitraum wurde hinzugefügt.');
$this->logger->info('Add availability', [
'availability' => $availability,
]);
$redirectUrl = $this->generateUrl('app_teamer_profile_availability_index');
$response->setCloseAndRedirect($redirectUrl);
} else {
$content = $this->renderView('teamer/profile/availability/create.html.twig', [
'form' => $form,
]);
$response->setContent($content);
}
return $this->json($response);
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Controller\Teamer\Profile\Availability;
use App\Entity\Availability;
use App\Entity\User;
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 DeleteController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/teamer/profile/availability/delete/{id}', name: 'app_teamer_profile_availability_delete')]
#[IsGranted('DELETE', subject: 'availability')]
public function index(Availability $availability): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
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', [
'user' => $this->getUser()->getUserIdentifier(),
]);
return $this->redirectToRoute('app_teamer_profile_availability_index');
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Controller\Teamer\Profile\Availability;
use App\Entity\Availability;
use App\Entity\User;
use App\Repository\AvailabilityRepository;
use Carbon\Carbon;
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
{
public function __construct(private readonly AvailabilityRepository $availabilityRepository)
{}
#[Route('/teamer/profile/availabilities', name: 'app_teamer_profile_availability_index')]
#[IsGranted('ROLE_TEAMER')]
public function index(): Response
{
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
$availabilities = $teamer->getAvailabilities()->toArray();
$selectedAvailabilities = $this->groupRecords($availabilities);
$availabilities = $this->availabilityRepository->getSelectableForTeamer($teamer);
$selectableAvailabilities = $this->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;
}
}
+11 -2
View File
@@ -9,6 +9,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: AvailabilityRepository::class)]
class Availability implements BlameableEntityInterface, TimestampableEntityInterface
@@ -22,9 +23,12 @@ class Availability implements BlameableEntityInterface, TimestampableEntityInter
private ?int $id = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
#[Assert\NotNull(message: 'Bitte gib das Datum an')]
private ?\DateTimeImmutable $dateFrom = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
#[Assert\NotNull(message: 'Bitte gib das Datum an')]
#[Assert\GreaterThan(propertyPath: 'dateFrom', message: 'Das Datum muss nach dem Beginndatum liegen')]
private ?\DateTimeImmutable $dateTo = null;
#[ORM\ManyToOne(inversedBy: 'customAvailabilities')]
@@ -38,6 +42,11 @@ class Availability implements BlameableEntityInterface, TimestampableEntityInter
$this->teamers = new ArrayCollection();
}
public function __toString()
{
return sprintf('%s - %s', $this->getDateFrom()->format('d.m.Y'), $this->getDateTo()->format('d.m.Y'));
}
public function getId(): ?int
{
return $this->id;
@@ -48,7 +57,7 @@ class Availability implements BlameableEntityInterface, TimestampableEntityInter
return $this->dateFrom;
}
public function setDateFrom(\DateTimeImmutable $dateFrom): static
public function setDateFrom(?\DateTimeImmutable $dateFrom): static
{
$this->dateFrom = $dateFrom;
@@ -60,7 +69,7 @@ class Availability implements BlameableEntityInterface, TimestampableEntityInter
return $this->dateTo;
}
public function setDateTo(\DateTimeImmutable $dateTo): static
public function setDateTo(?\DateTimeImmutable $dateTo): static
{
$this->dateTo = $dateTo;
+1
View File
@@ -84,6 +84,7 @@ class Teamer implements TimestampableEntityInterface
private ?string $remarks = null;
#[ORM\ManyToMany(targetEntity: Availability::class, inversedBy: 'teamers')]
#[ORM\OrderBy(['dateFrom' => 'ASC'])]
private Collection $availabilities;
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Availability::class)]
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Form;
use App\Entity\Availability;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AvailabilityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('dateFrom', DatepickerType::class, [
'label' => 'Datum von',
'min_date' => new \DateTimeImmutable(),
])
->add('dateTo', DatepickerType::class, [
'label' => 'Datum bis',
'min_date' => new \DateTimeImmutable(),
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Availability::class,
]);
}
}
+9 -22
View File
@@ -41,35 +41,17 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'title' => 'Einstellungen',
'icon' => 'settings',
'children' => [
[
'route' => 'app_admin_system_availability_index',
'title' => 'Verfügbarkeiten',
],
[
'route' => 'app_admin_system_fee_index',
'title' => 'Honorare',
'children' => [
[
'route' => 'app_admin_system_fee_create',
'title' => 'Honorar anlegen',
],
[
'route' => 'app_admin_system_fee_edit',
'routeParameters' => $this->getDefaultRouteParameters(),
'title' => 'Honorar bearbeiten',
],
],
],
[
'route' => 'app_admin_system_training_index',
'title' => 'Fortbildungen',
'children' => [
[
'route' => 'app_admin_system_training_create',
'title' => 'Fortbildung anlegen',
],
[
'route' => 'app_admin_system_training_edit',
'routeParameters' => $this->getDefaultRouteParameters(),
'title' => 'Fortbildung bearbeiten',
],
],
],
[
'route' => 'app_admin_system_job_profile_index',
@@ -79,6 +61,11 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'route' => 'app_admin_system_job_profile_create',
'title' => 'Job-Profil anlegen',
],
[
'route' => 'app_admin_system_job_profile_edit',
'routeParameters' => $this->getDefaultRouteParameters(),
'title' => 'Job-Profil bearbeiten',
],
],
],
[
+1 -1
View File
@@ -28,7 +28,7 @@ class TeamerMenuBuilder extends AbstractMenuBuilder
'title' => 'Jobprofile &amp; Skills',
],
[
'route' => 'app_teamer_profile_availabilities',
'route' => 'app_teamer_profile_availability_index',
'title' => 'Meine Verfügbarkeit',
],
],
+17 -1
View File
@@ -5,6 +5,7 @@ namespace App\Repository;
use App\Entity\Availability;
use App\Entity\Teamer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\Persistence\ManagerRegistry;
/**
@@ -22,7 +23,22 @@ class AvailabilityRepository extends ServiceEntityRepository
parent::__construct($registry, Availability::class);
}
public function getSelectable(Teamer $teamer): array
public function getListQuery(): Query
{
$qb = $this->createQueryBuilder('availability');
return $qb
->where($qb->expr()->andX(
$qb->expr()->gt('availability.dateFrom', ':today'),
$qb->expr()->isNull('availability.owner')
))
->orderBy('availability.dateFrom', 'ASC')
->setParameter('today', new \DateTimeImmutable())
->getQuery()
;
}
public function getSelectableForTeamer(Teamer $teamer): array
{
$qb = $this->createQueryBuilder('availability');
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Security\Voter;
use App\Entity\Availability;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class AvailabilityVoter extends Voter
{
public const ASSIGN = 'ASSIGN';
public const DELETE = 'DELETE';
public function __construct(private readonly Security $security)
{}
protected function supports(string $attribute, mixed $subject): bool
{
if (!$subject instanceof Availability) {
return false;
}
return in_array($attribute, [static::ASSIGN, static::DELETE]);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
/** @var Availability $availability */
$availability = $subject;
if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
return true;
}
/** @var User $user */
$user = $this->security->getUser();
$teamer = $user->getTeamer();
return null === $availability->getOwner() || $teamer === $availability->getOwner();
}
}
@@ -0,0 +1,10 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -0,0 +1,68 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Einstellungen - Verfügbarkeiten/Zeiträume{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Verfügbarkeiten/Zeiträume
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th class="w-1/3">
{{ knp_pagination_sortable(pagination, 'Datum von', 'availability.dateFrom') }}
</th>
<th class="w-1/3">
{{ knp_pagination_sortable(pagination, 'Datum bis', 'availability.dateTo') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for availability in pagination %}
<tr>
<td>
{{ availability.dateFrom|date('d.m.Y') }}
</td>
<td>
{{ availability.dateTo|date('d.m.Y') }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
<button type="button"
class="text-red-500"
{{ 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_system_availability_delete', { 'id': availability.id })
}) }}>
{{ icon('delete') }}
</button>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">
<div class="text-sm">Keine Daten...</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
<button type="button"
class="btn"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Zeitraum hinzufügen',
'url': path('app_admin_system_availability_create')
}) }}>
Zeitraum hinzufügen
</button>
{% endblock %}
@@ -1,15 +0,0 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.name) }}
{{ form_row(form.value) }}
</div>
<div class="flex items-center space-x-2">
<button type="submit" class="btn">
Speichern
</button>
<a href="{{ path('app_admin_system_fee_index') }}" class="btn btn--secondary">
Abbrechen
</a>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
+10 -12
View File
@@ -1,12 +1,10 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Einstellungen - Honorar anlegen{% endblock %}
{% block content %}
<div class="max-w-screen-sm">
<h1 class="text-2xl font-bold pb-8">
Honorar anlegen
</h1>
{% include 'admin/system/fee/_form.html.twig' %}
</div>
{% endblock %}
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.name) }}
{{ form_row(form.value) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
-12
View File
@@ -1,12 +0,0 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Einstellungen - Honorar bearbeiten{% endblock %}
{% block content %}
<div class="max-w-screen-sm">
<h1 class="text-2xl font-bold pb-8">
Honorar bearbeiten
</h1>
{% include 'admin/system/fee/_form.html.twig' %}
</div>
{% endblock %}
+58 -55
View File
@@ -3,62 +3,65 @@
{% block title %}Einstellungen - Honorare{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Honorare
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<h1 class="text-2xl font-bold pb-8">
Honorare
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
Bezeichnung
</th>
<th>
Wert
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for fee in fees %}
<tr>
<th>
Bezeichnung
</th>
<th>
Wert
</th>
<th></th>
<td>
{{ fee.name }}
</td>
<td>
{{ fee.value|format_money }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
<button type="button"
class="text-red-500"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du das Honorar wirklich löschen?',
'target-url': path('app_admin_system_fee_delete', { 'id': fee.id })
}) }}>
{{ icon('delete') }}
</button>
</div>
</td>
</tr>
</thead>
<tbody>
{% for fee in fees %}
<tr>
<td>
{{ fee.name }}
</td>
<td>
{{ fee.value|format_money }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
<button type="button"
class="text-red-500"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du das Honorar wirklich löschen?',
'target-url': path('app_admin_system_fee_delete', { 'id': fee.id })
}) }}>
{{ icon('delete') }}
</button>
<a href="{{ path('app_admin_system_fee_edit', { 'id': fee.id }) }}">
{{ icon('edit') }}
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">
<div class="text-sm">Keine Daten...</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<tr>
<td colspan="3">
<div class="text-sm">Keine Daten...</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ path('app_admin_system_fee_create') }}" class="btn">
Neu
</a>
</div>
<button type="button"
class="btn"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Honorar hinzufügen',
'url': path('app_admin_system_fee_create')
}) }}>
Honorar hinzufügen
</button>
{% endblock %}
@@ -1,14 +0,0 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.name) }}
</div>
<div class="flex items-center space-x-2">
<button type="submit" class="btn">
Speichern
</button>
<a href="{{ path('app_admin_system_training_index') }}" class="btn btn--secondary">
Abbrechen
</a>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -1,12 +1,9 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Einstellungen - Fortbildung anlegen{% endblock %}
{% block content %}
<div class="max-w-screen-sm">
<h1 class="text-2xl font-bold pb-8">
Fortbildung anlegen
</h1>
{% include 'admin/system/training/_form.html.twig' %}
</div>
{% endblock %}
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.name) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -1,12 +0,0 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Einstellungen - Fortbildung bearbeiten{% endblock %}
{% block content %}
<div class="max-w-screen-sm">
<h1 class="text-2xl font-bold pb-8">
Fortbildung bearbeiten
</h1>
{% include 'admin/system/training/_form.html.twig' %}
</div>
{% endblock %}
+58 -55
View File
@@ -3,62 +3,65 @@
{% block title %}Einstellungen - Fortbildungen{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Fortbildungen
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<h1 class="text-2xl font-bold pb-8">
Fortbildungen
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
Bezeichnung
</th>
<th>
Teilnahmen
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for training in trainings %}
<tr>
<th>
Bezeichnung
</th>
<th>
Teilnahmen
</th>
<th></th>
<td>
{{ training.name }}
</td>
<td>
{{ training.trainingAttendances|length|default('-') }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
<button type="button"
class="text-red-500"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du die Fortbildung wirklich löschen?',
'target-url': path('app_admin_system_training_delete', { 'id': training.id })
}) }}>
{{ icon('delete') }}
</button>
</div>
</td>
</tr>
</thead>
<tbody>
{% for training in trainings %}
<tr>
<td>
{{ training.name }}
</td>
<td>
{{ training.trainingAttendances|length|default('-') }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
<button type="button"
class="text-red-500"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du die Fortbildung wirklich löschen?',
'target-url': path('app_admin_system_training_delete', { 'id': training.id })
}) }}>
{{ icon('delete') }}
</button>
<a href="{{ path('app_admin_system_training_edit', { 'id': training.id }) }}">
{{ icon('edit') }}
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">
<div class="text-sm">Keine Daten...</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<tr>
<td colspan="3">
<div class="text-sm">Keine Daten...</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ path('app_admin_system_training_create') }}" class="btn">
Neu
</a>
</div>
<button type="button"
class="btn"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Fortbildung hinzufügen',
'url': path('app_admin_system_training_create')
}) }}>
Fortbildung hinzufügen
</button>
{% endblock %}
+1 -1
View File
@@ -1,7 +1,7 @@
<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>
<p class="text-sm leading-5 text-gray-700">{{ 'label.paginator.info'|trans({ '{from}': firstItemNumber, '{to}': lastItemNumber, '{total}': totalCount })|raw }}</p>
<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">
@@ -1,39 +0,0 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Meine Verfügbarkeit{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-4">
Meine Vergfügbarkeit
</h1>
<p class="pb-8">
Hier kannst du angeben, wann du allgemein Zeit und Lust hättest, Einsätze zu absolvieren. Wenn wir in diesem
Zeitraum einen Einsatz zu vergeben haben, dann nehmen wir telefonisch oder per E-Mail Kontakt zu dir auf.
</p>
<h2 class="text-xl font-bold pb-2">
Ich bin verfügbar
</h2>
<ul class="list-disc pl-4 pb-8">
{% for availability in availabilities %}
<li>
{{ availability.dateFrom|date('d.m.Y') }} - {{ availability.dateTo|date('d.m.Y') }}
</li>
{% else %}
<li class="text-sm">
Du hast bisher keine Verfügbarkeiten hinterlegt.
</li>
{% endfor %}
</ul>
<h2 class="text-xl font-bold pb-2">
Weitere Verfügbarkeiten
</h2>
{{ form_start(form) }}
{{ form_row(form.availabilities) }}
<button type="submit" class="btn">
Aktualisieren
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,10 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -0,0 +1,99 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Meine Verfügbarkeit{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-4">
Meine Vergfügbarkeit
</h1>
<p class="pb-8">
Hier kannst du angeben, wann du allgemein Zeit und Lust hättest, Einsätze zu absolvieren. Wenn wir in diesem
Zeitraum einen Einsatz zu vergeben haben, dann nehmen wir telefonisch oder per E-Mail Kontakt zu dir auf.
</p>
<h2 class="text-xl font-bold pb-2">
Ich bin verfügbar
</h2>
<div class="grid md:grid-cols-2 gap-8 pb-8">
{% for year, months in selectedAvailabilities %}
<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_teamer_profile_availability_delete', { 'id': availability.id })
}) }}>
{{ icon('delete', 'w-5 h-5') }}
</button>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="md:col-span-2 text-sm">
Du hast bisher keine Verfügbarkeiten hinterlegt.
</div>
{% endfor %}
</div>
<h2 class="text-xl font-bold pb-2">
Weitere Verfügbarkeiten
</h2>
<div class="grid md:grid-cols-2 gap-8 pb-8">
{% for year, months in selectableAvailabilities %}
<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>
<a href="{{ path('app_teamer_profile_availability_add', { 'id': availability.id }) }}"
title="Zeitraum hinzufügen">
{{ icon('plus', 'w-5 h-5') }}
</a>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="md:col-span-2 text-sm">
Es sind keine weiteren Zeiträume verfügbar.
</div>
{% endfor %}
</div>
<button type="button"
class="btn"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Zeitraum hinzufügen',
'url': path('app_teamer_profile_availability_create')
}) }}>
Eigenen Zeitraum hinzufügen
</button>
{% endblock %}
+5 -1
View File
@@ -3,4 +3,8 @@ uploader:
click_here: hier klicken
file_too_big: Die maximale Dateigröße beträgt %maxFilesize%MB
invalid_file_type: Der Dateityp ist nicht erlaubt
max_files_exceeded: Die maximale Anzahl von Dateien is %maxFilesExceeded%
max_files_exceeded: Die maximale Anzahl von Dateien is %maxFilesExceeded%
paginator:
previous: vorherige
next: nächste
info: 'Einträge <span class="font-medium">{from}</span> bis <span class="font-medium">{to}</span> von {total}'