WIP Impelement more stuff

This commit is contained in:
Björn Fromme
2023-10-04 15:10:16 +02:00
parent dec8feedf2
commit cc1ab396af
35 changed files with 928 additions and 177 deletions
+3
View File
@@ -26,6 +26,9 @@
<symbol id="icon-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <symbol id="icon-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</symbol> </symbol>
<symbol id="icon-minus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</symbol>
<symbol id="icon-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <symbol id="icon-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</symbol> </symbol>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+3
View File
@@ -63,6 +63,9 @@ services:
- name: knp_menu.menu_builder - name: knp_menu.menu_builder
method: createMainMenu method: createMainMenu
alias: admin_main alias: admin_main
- name: knp_menu.menu_builder
method: createTeamerMenu
alias: admin_teamer
App\Menu\ManagerMenuBuilder: App\Menu\ManagerMenuBuilder:
arguments: arguments:
+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 Version20231004073716 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('DROP TABLE period');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE period (id INT AUTO_INCREMENT NOT NULL, date_from DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', date_to DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\', created_by VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, updated_by VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
}
}
+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 Version20231004074116 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 teamer CHANGE language language VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE teamer CHANGE language language VARCHAR(255) NOT NULL');
}
}
+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 Version20231004074136 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 teamer CHANGE size size VARCHAR(2) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE teamer CHANGE size size VARCHAR(2) NOT NULL');
}
}
+2 -2
View File
@@ -32,8 +32,8 @@ class Countries
return $countries; return $countries;
} }
public function get(int $id): ?Country public function get(string $token): ?Country
{ {
return $this->getAll()[$id] ?? null; return $this->getAll()[$token] ?? null;
} }
} }
+3 -3
View File
@@ -176,15 +176,15 @@ class ResponseParser
$countries = []; $countries = [];
foreach ($xml->xpath('laender/land') as $item) { foreach ($xml->xpath('laender/land') as $item) {
$id = (int) $item->attributes()['id']; $token = (string) $item->attributes()['kuerzel'];
$country = new Country(); $country = new Country();
$country $country
->setId($id) ->setId((int) $item->attributes()['id'])
->setName((string) $item->attributes()['bezeichnung']) ->setName((string) $item->attributes()['bezeichnung'])
->setToken((string) $item->attributes()['kuerzel']) ->setToken((string) $item->attributes()['kuerzel'])
->setNationality((string) $item->attributes()['nationalitaet']) ->setNationality((string) $item->attributes()['nationalitaet'])
; ;
$countries[$id] = $country; $countries[$token] = $country;
} }
$response = new BaseDataResponse(); $response = new BaseDataResponse();
@@ -0,0 +1,21 @@
<?php
namespace App\Controller\Admin\Teamer;
use App\Entity\Teamer;
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 AvailabilityController extends AbstractController
{
#[Route('/admin/teamer/availability/{uuid}', name: 'app_admin_teamer_availability')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Teamer $teamer): Response
{
return $this->render('admin/teamer/availability.html.twig', [
'teamer' => $teamer,
]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Controller\Admin\Teamer;
use App\Entity\Teamer;
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 CrmSelectionsController extends AbstractController
{
#[Route('/admin/teamer/crm-selections/{uuid}', name: 'app_admin_teamer_crm_selections')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Teamer $teamer): Response
{
return $this->render('admin/teamer/crm_selections.html.twig', [
'teamer' => $teamer,
]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Controller\Admin\Teamer;
use App\Entity\Teamer;
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 ProfileController extends AbstractController
{
#[Route('/admin/teamer/profile/{uuid}', name: 'app_admin_teamer_profile')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Teamer $teamer): Response
{
return $this->render('admin/teamer/profile.html.twig', [
'teamer' => $teamer,
]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Controller\Admin\Teamer;
use App\Entity\Teamer;
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 RecentAssignmentsController extends AbstractController
{
#[Route('/admin/teamer/recent-assignments/{uuid}', name: 'app_admin_teamer_recent_assignments')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Teamer $teamer): Response
{
return $this->render('admin/teamer/recent_assignments.html.twig', [
'teamer' => $teamer,
]);
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Admin\Teamer;
use App\Entity\Teamer;
use App\Repository\TrainingRepository;
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 SkillsController extends AbstractController
{
public function __construct(private readonly TrainingRepository $trainingRepository)
{}
#[Route('/admin/teamer/skills/{uuid}', name: 'app_admin_teamer_skills')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Teamer $teamer): Response
{
$trainings = $this->trainingRepository->getList();
return $this->render('admin/teamer/skills.html.twig', [
'teamer' => $teamer,
'trainings' => $trainings,
]);
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Controller\Admin\Teamer\TrainingAttendance;
use App\Entity\Teamer;
use App\Entity\Training;
use App\Entity\TrainingAttendance;
use App\Form\AbbreviatedDateType;
use App\Form\FeeType;
use App\Model\AjaxModalResponseDto;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
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/teamer/skills/training-attendance/create/{training_id}/{teamer_id}', name: 'app_admin_teamer_skills_training_attendance_create')]
#[IsGranted('ROLE_ADMIN')]
public function index(
#[MapEntity(mapping: ['training_id' => 'id'])]
Training $training,
#[MapEntity(mapping: ['teamer_id' => 'id'])]
Teamer $teamer,
Request $request
): JsonResponse {
$response = new AjaxModalResponseDto();
$action = $this->generateUrl('app_admin_teamer_skills_training_attendance_create', [
'training_id' => $training->getId(),
'teamer_id' => $teamer->getId(),
]);
$attendance = new TrainingAttendance();
$attendance
->setTeamer($teamer)
->setTraining($training)
;
$form = $this
->createFormBuilder($attendance, ['action' => $action, 'ajax_submit' => true])
->add('date', AbbreviatedDateType::class, [
'label' => 'Datum',
])
->getForm()
;
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($attendance);
$this->entityManager->flush();
$this->addFlash('success', 'Die Teilnahme wurde hinzugefügt');
$this->logger->info('Create training attendance', [
'teamer' => $teamer->getUuid(),
'training' => $training->getName(),
]);
$redirectUrl = $this->generateUrl('app_admin_teamer_skills', ['uuid' => $teamer->getUuid()]);
$response->setCloseAndRedirect($redirectUrl);
} else {
$content = $this->renderView('admin/teamer/training_attendance/create.html.twig', [
'form' => $form
]);
$response->setContent($content);
}
return $this->json($response);
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Controller\Admin\Teamer\TrainingAttendance;
use App\Entity\TrainingAttendance;
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/teamer/skills/training-attendance/delete/{uuid}', name: 'app_admin_teamer_skills_training_attendance_delete', methods: ['POST'])]
#[IsGranted('ROLE_ADMIN')]
public function index(TrainingAttendance $attendance): Response
{
$teamer = $attendance->getTeamer();
$this->entityManager->remove($attendance);
$this->entityManager->flush();
$this->addFlash('success', 'Die Teilnahme an der Fortbilundg wurde gelöscht');
$this->logger->info('Delete training attendance', [
'teamer' => $teamer->getUuid(),
]);
return $this->redirectToRoute('app_admin_teamer_skills', ['uuid' => $teamer->getUuid()]);
}
}
+15 -1
View File
@@ -2,17 +2,31 @@
namespace App\Controller\Teamer; namespace App\Controller\Teamer;
use App\Entity\User;
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;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class IndexController extends AbstractController class IndexController extends AbstractController
{ {
public function __construct(private readonly ValidatorInterface $validator)
{}
#[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(): Response
{ {
return $this->render('teamer/index.html.twig'); /** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
// Validate teamer data to show missing data right away
$errors = $this->validator->validate($teamer, null, ['profile_preflight']);
return $this->render('teamer/index.html.twig', [
'errors' => $errors,
]);
} }
} }
@@ -35,11 +35,11 @@ class SkillsController extends AbstractController
foreach ($jobProfiles as $profile) { foreach ($jobProfiles as $profile) {
$selectableJobProfiles[$profile->getId()] = true; $selectableJobProfiles[$profile->getId()] = true;
if (null !== $profile->getRequiredTraining() && false === $teamer->hasTraining($profile->getRequiredTraining())) { if (null !== $profile->getRequiredTraining() && null === $teamer->getTrainingAttendance($profile->getRequiredTraining())) {
$selectableJobProfiles[$profile->getId()] = false; $selectableJobProfiles[$profile->getId()] = false;
} }
foreach ($profile->getRequiredLicenses() as $license) { foreach ($profile->getRequiredLicenses() as $license) {
if (false === $teamer->hasLicenseOfType($license)) { if (null === $teamer->getLicenseOfType($license)) {
$selectableJobProfiles[$profile->getId()] = false; $selectableJobProfiles[$profile->getId()] = false;
} }
} }
-56
View File
@@ -1,56 +0,0 @@
<?php
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\PeriodRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PeriodRepository::class)]
class Period implements BlameableEntityInterface, TimestampableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
private ?\DateTimeImmutable $dateFrom = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
private ?\DateTimeImmutable $dateTo = null;
public function getId(): ?int
{
return $this->id;
}
public function getDateFrom(): ?\DateTimeImmutable
{
return $this->dateFrom;
}
public function setDateFrom(\DateTimeImmutable $dateFrom): static
{
$this->dateFrom = $dateFrom;
return $this;
}
public function getDateTo(): ?\DateTimeImmutable
{
return $this->dateTo;
}
public function setDateTo(\DateTimeImmutable $dateTo): static
{
$this->dateTo = $dateTo;
return $this;
}
}
+22 -8
View File
@@ -121,11 +121,11 @@ class Teamer implements TimestampableEntityInterface
#[ORM\Column] #[ORM\Column]
private array $pickups = []; private array $pickups = [];
#[ORM\Column(length: 255)] #[ORM\Column(length: 255, nullable: true)]
#[Assert\NotBlank(message: 'Bitte gib deine Sprache(n) an', groups: ['profile', 'profile_preflight'])] #[Assert\NotBlank(message: 'Bitte gib deine Sprache(n) an', groups: ['profile', 'profile_preflight'])]
private ?string $language = null; private ?string $language = null;
#[ORM\Column(length: 2)] #[ORM\Column(length: 2, nullable: true)]
private ?string $size = null; private ?string $size = null;
public function __construct() public function __construct()
@@ -143,6 +143,11 @@ class Teamer implements TimestampableEntityInterface
$this->customAvailabilities = new ArrayCollection(); $this->customAvailabilities = new ArrayCollection();
} }
public function __toString()
{
return $this->getFullName();
}
public function toPayload(): array public function toPayload(): array
{ {
// Transform gender value // Transform gender value
@@ -224,6 +229,15 @@ class Teamer implements TimestampableEntityInterface
return $this; return $this;
} }
public function getFullName(bool $formal = false): string
{
if (true === $formal) {
return sprintf('%s, %s', $this->getLastName(), $this->getFirstName());
}
return sprintf('%s %s', $this->getFirstName(), $this->getLastName());
}
public function getGender(): ?string public function getGender(): ?string
{ {
return $this->gender; return $this->gender;
@@ -494,15 +508,15 @@ class Teamer implements TimestampableEntityInterface
return $this; return $this;
} }
public function hasTraining(Training $training): bool public function getTrainingAttendance(Training $training): ?TrainingAttendance
{ {
foreach ($this->getTrainingAttendances() as $attendance) { foreach ($this->getTrainingAttendances() as $attendance) {
if ($attendance->getTraining() === $training) { if ($attendance->getTraining() === $training) {
return true; return $attendance;
} }
} }
return false; return null;
} }
/** /**
@@ -535,15 +549,15 @@ class Teamer implements TimestampableEntityInterface
return $this; return $this;
} }
public function hasLicenseOfType(string $type): bool public function getLicenseOfType(string $type): ?License
{ {
foreach ($this->getLicenses() as $license) { foreach ($this->getLicenses() as $license) {
if ($type === $license->getType()) { if ($type === $license->getType()) {
return true; return $license;
} }
} }
return false; return null;
} }
/** /**
+1 -1
View File
@@ -29,7 +29,7 @@ class TeamerJobProfileType extends AbstractType
$requirements[].= $training->getName(); $requirements[].= $training->getName();
} }
foreach ($choice->getRequiredLicenses() as $license) { foreach ($choice->getRequiredLicenses() as $license) {
$requirements[] = sprintf('Offizielle %slehrer*innenlizenz', ucfirst($license)); $requirements[] = sprintf('Offizielle %slehrer*innenlizenz', ucfirst(strtolower($license)));
} }
if ($requirements) { if ($requirements) {
+52 -4
View File
@@ -6,9 +6,9 @@ use Knp\Menu\ItemInterface;
class AdminMenuBuilder extends AbstractMenuBuilder class AdminMenuBuilder extends AbstractMenuBuilder
{ {
public function createMainMenu(array $options): ItemInterface private function getMainMenuItems(): array
{ {
$menuItems = [ return [
[ [
'route' => 'app_admin_index', 'route' => 'app_admin_index',
'title' => 'Dashboard', 'title' => 'Dashboard',
@@ -34,7 +34,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'title' => 'Teamerübersicht', 'title' => 'Teamerübersicht',
'icon' => 'users', 'icon' => 'users',
'hideChildren' => true, 'hideChildren' => true,
'children' => [], 'children' => $this->getTeamerMenuItems(),
], ],
[ [
'route' => false, 'route' => false,
@@ -86,8 +86,42 @@ class AdminMenuBuilder extends AbstractMenuBuilder
], ],
], ],
]; ];
}
$menu = $this->createMenu($menuItems); private function getTeamerMenuItems(): array
{
return [
[
'route' => 'app_admin_teamer_profile',
'title' => 'Stammdaten &amp; Foto',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
],
[
'route' => 'app_admin_teamer_skills',
'title' => 'Jobprofile &amp; Skills',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
],
[
'route' => 'app_admin_teamer_availability',
'title' => 'Verfügbarkeit',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
],
[
'route' => 'app_admin_teamer_crm_selections',
'title' => 'Selektionsmerkmale',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
],
[
'route' => 'app_admin_teamer_recent_assignments',
'title' => 'Vergangene Einsätze',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
],
];
}
public function createMainMenu(array $options): ItemInterface
{
$menu = $this->createMenu($this->getMainMenuItems());
$this->addDivider($menu); $this->addDivider($menu);
@@ -100,4 +134,18 @@ class AdminMenuBuilder extends AbstractMenuBuilder
return $menu; return $menu;
} }
public function createTeamerMenu(array $options): ItemInterface
{
$menu = $this->createMenu($this->getTeamerMenuItems());
$this->addDivider($menu);
$menu->addChild('zurück zur Übersicht', [
'route' => 'app_admin_teamer_index',
'icon' => 'back',
]);
return $menu;
}
} }
-66
View File
@@ -1,66 +0,0 @@
<?php
namespace App\Repository;
use App\Entity\Period;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Period>
*
* @method Period|null find($id, $lockMode = null, $lockVersion = null)
* @method Period|null findOneBy(array $criteria, array $orderBy = null)
* @method Period[] findAll()
* @method Period[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PeriodRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Period::class);
}
public function save(Period $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Period $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return Period[] Returns an array of Period objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Period
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+4
View File
@@ -15,6 +15,10 @@ class AppExtension extends AbstractExtension
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']),
new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']), new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']),
new TwigFilter('license_label', [AppRuntime::class, 'licenseLabel']),
new TwigFilter('teamer_status_label', [AppRuntime::class, 'teamerStatusLabel']),
new TwigFilter('bpn_country_label', [AppRuntime::class, 'bpnCountryLabel']),
new TwigFilter('bpn_pickup_label', [AppRuntime::class, 'bpnPickupLabel']),
]; ];
} }
+42
View File
@@ -2,6 +2,10 @@
namespace App\Twig; namespace App\Twig;
use App\BusProNet\DataProvider\Countries;
use App\BusProNet\DataProvider\Pickups;
use App\BusProNet\Model\Country;
use App\Entity\Teamer;
use Carbon\Carbon; use Carbon\Carbon;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Environment; use Twig\Environment;
@@ -13,10 +17,48 @@ 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 Countries $countries,
private readonly Pickups $pickups,
private readonly string $environment private readonly string $environment
) { ) {
} }
public function teamerStatusLabel(string $status): string
{
if (Teamer::STATUS_NEW === $status) {
return 'Neuteamer';
}
return 'Bestandsteamer';
}
public function licenseLabel(string $type): string
{
return sprintf('Offizielle %slehrer*innenlizenz', ucfirst(strtolower($type)));
}
public function bpnCountryLabel(string $token, string $property = 'name'): string
{
$country = $this->countries->get($token);
if (null === $country) {
return 'unbekannt';
}
return 'nationality' === $property ? $country->getNationality() : $country->getName();
}
public function bpnPickupLabel(int $id): string
{
$pickup = $this->pickups->get($id);
if (null === $pickup) {
return 'unbekannt';
}
return $pickup->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) {
+7 -7
View File
@@ -3,13 +3,13 @@
{% block list %} {% block list %}
{% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %} {% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
{% if item.level == 0 %} {% if item.level == 0 %}
<div class="min-h-full flex grow flex-col gap-y-5 overflow-y-scroll lg:border-r border-gray-200 bg-white md:pr-6"> <div class="min-h-full flex grow flex-col gap-y-5 overflow-y-scroll lg:border-r border-gray-200 bg-white md:pr-6">
<nav class="flex flex-1 flex-col"> <nav class="flex flex-1 flex-col">
<ul role="list" class="space-y-1"> <ul role="list" class="space-y-1">
{{ block('children') }} {{ block('children') }}
</ul> </ul>
</nav> </nav>
</div> </div>
{% else %} {% else %}
<ul class="{{ html_classes('mt-1 px-2', { 'hidden': not matcher.isCurrent(item) }) }}" {{ stimulus_target('sidebar-menu', 'submenu') }}> <ul class="{{ html_classes('mt-1 px-2', { 'hidden': not matcher.isCurrent(item) }) }}" {{ stimulus_target('sidebar-menu', 'submenu') }}>
{{ block('children') }} {{ block('children') }}
@@ -0,0 +1,16 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Verfügbarkeit {{ teamer }}{% endblock %}
{% block content %}
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-8">
<div>
{{ knp_menu_render(knp_menu_get('admin_teamer')) }}
</div>
<div class="lg:col-span-2">
<h1 class="text-2xl font-bold pb-8">
Verfügbarkeit {{ teamer }}
</h1>
</div>
</div>
{% endblock %}
@@ -0,0 +1,16 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Selektionsmerkmale {{ teamer }}{% endblock %}
{% block content %}
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-8">
<div>
{{ knp_menu_render(knp_menu_get('admin_teamer')) }}
</div>
<div class="lg:col-span-2">
<h1 class="text-2xl font-bold pb-8">
Selektionsmerkmale {{ teamer }}
</h1>
</div>
</div>
{% endblock %}
+4 -1
View File
@@ -43,7 +43,7 @@
</a> </a>
</td> </td>
<td> <td>
{{ teamer.status }} {{ teamer.status|teamer_status_label }}
</td> </td>
<td> <td>
{{ teamer.communication.email }} {{ teamer.communication.email }}
@@ -57,6 +57,9 @@
</td> </td>
<td> <td>
<div class="flex items-center space-x-1 justify-end"> <div class="flex items-center space-x-1 justify-end">
<a href="{{ path('app_admin_teamer_profile', { 'uuid': teamer.uuid }) }}" title="Teamerprofil {{ teamer }}">
{{ icon('user') }}
</a>
</div> </div>
</td> </td>
</tr> </tr>
+230
View File
@@ -0,0 +1,230 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Stammdaten und Foto {{ teamer }}{% endblock %}
{% block content %}
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-8">
<div>
{{ knp_menu_render(knp_menu_get('admin_teamer')) }}
</div>
<div class="lg:col-span-2">
<h1 class="text-2xl font-bold pb-8">
Stammdaten und Foto {{ teamer }}
</h1>
<div class="grid grid-cols-2 gap-4">
<div>
<h2 class="text-lg font-bold pb-2">
Persönliche Daten
</h2>
<div class="flex flex-col space-y-3">
<div>
<div class="font-bold">
Gender:
</div>
<div>
{{ teamer.gender }}
</div>
</div>
<div>
<div class="font-bold">
Titel:
</div>
<div>
{{ teamer.academicTitle|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
Vorname:
</div>
<div>
{{ teamer.firstName }}
</div>
</div>
<div>
<div class="font-bold">
Nachname:
</div>
<div>
{{ teamer.lastName }}
</div>
</div>
<div>
<div class="font-bold">
Geburtsdatum:
</div>
<div>
{{ teamer.dateOfBirth|date('d.m.Y') }}
</div>
</div>
<div>
<div class="font-bold">
Staatsangehörigkeit:
</div>
<div>
{{ teamer.nationality|bpn_country_label('nationality') }}
</div>
</div>
</div>
</div>
<div>
{% if teamer.photo %}
<div class="pb-8">
<img src="{{ asset(teamer.photo.filename | imagine_filter('profile')) }}"
class="w-48 h-auto border-2 border-primary"
alt="{{ teamer.firstName }}">
</div>
{% endif %}
</div>
<div>
<h2 class="text-lg font-bold pb-2">
Adressdaten
</h2>
<div class="flex flex-col space-y-3">
<div>
<div class="font-bold">
Straße, Hausnummer:
</div>
<div>
{{ teamer.address.street }}
</div>
</div>
<div>
<div class="font-bold">
PLZ:
</div>
<div>
{{ teamer.address.postCode }}
</div>
</div>
<div>
<div class="font-bold">
Ort:
</div>
<div>
{{ teamer.address.city }}
</div>
</div>
<div>
<div class="font-bold">
Land:
</div>
<div>
{{ teamer.address.country|bpn_country_label }}
</div>
</div>
<div>
<div class="font-bold">
E-Mail:
</div>
<div>
{{ teamer.communication.email|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
Mobilnummer:
</div>
<div>
{{ teamer.communication.mobile|default('-') }}
</div>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-bold pb-2">
Buszustiege
</h2>
<ul class="list-disc pl-4">
{% for pickup in teamer.pickups %}
<li>
{{ pickup|bpn_pickup_label }}
</li>
{% else %}
<li>
<span class="text-sm">Keine Daten</span>
</li>
{% endfor %}
</ul>
</div>
<div>
<h2 class="text-lg font-bold pb-2">
Honorardaten
</h2>
<div class="flex flex-col space-y-3">
<div>
<div class="font-bold">
Steuer-ID:
</div>
<div>
{{ teamer.taxId|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
IBAN:
</div>
<div>
{{ teamer.bankAccount.iban|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
BIC:
</div>
<div>
{{ teamer.bankAccount.bic|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
Bank/Kreditinstitut:
</div>
<div>
{{ teamer.bankAccount.bank|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
Krankenversicherung:
</div>
<div>
{{ teamer.healthInsuranceCompany|default('-') }}
</div>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-bold pb-2">
Sonstiges
</h2>
<div class="flex flex-col space-y-3">
<div>
<div class="font-bold">
Sprache(n):
</div>
<div>
{{ teamer.language|default('-') }}
</div>
</div>
<div>
<div class="font-bold">
Klamottengröße:
</div>
<div>
{{ teamer.size|default('-') }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,16 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Vergangene Einsätze &amp; Feeback {{ teamer }}{% endblock %}
{% block content %}
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-8">
<div>
{{ knp_menu_render(knp_menu_get('admin_teamer')) }}
</div>
<div class="lg:col-span-2">
<h1 class="text-2xl font-bold pb-8">
Vergangene Einsätze &amp; Feeback {{ teamer }}
</h1>
</div>
</div>
{% endblock %}
+118
View File
@@ -0,0 +1,118 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Jobprofile &amp; Skills {{ teamer }}{% endblock %}
{% block content %}
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-8">
<div>
{{ knp_menu_render(knp_menu_get('admin_teamer')) }}
</div>
<div class="lg:col-span-2">
<h1 class="text-2xl font-bold pb-8">
Jobprofile &amp; Skills {{ teamer }}
</h1>
<h2 class="text-xl font-bold pb-2">
Ausbildung
</h2>
<ul class="pb-2 divide-y divide-gray-200">
{% for training in trainings %}
<li class="py-2">
<div class="flex items-center justify-between">
<span class="flex-1">{{ training.name }}</span>
{% set attendance = teamer.trainingAttendance(training) %}
{% if attendance %}
<div class="flex items-center space-x-2">
<span>{{ attendance.date|date('m/Y') }}</span>
<button type="button"
class="text-red-500"
title="Ausbildung löschen"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du die Teilnahme an dieser Schulung wirklich löschen?',
'target-url': path('app_admin_teamer_skills_training_attendance_delete', { 'uuid': attendance.uuid })
}) }}>
{{ icon('delete') }}
</button>
</div>
{% else %}
<button type="button"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Teilnahme hinzufügen',
'url': path('app_admin_teamer_skills_training_attendance_create', { 'training_id': training.id, 'teamer_id': teamer.id })
}) }}>
{{ icon('calendar', 'w-4 h-4') }}
</button>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
<ul class="pb-8 divide-y divide-gray-200">
{% for type in ['ski', 'snowboard'] %}
<li class="py-2">
<div class="flex items-center justify-between">
<span class="flex-1">{{ type|license_label }}</span>
{% set license = teamer.licenseOfType(type) %}
{% if license %}
<div class="flex items-center space-x-2">
<span>{{ license.date|date('m/Y') }}</span>
{% if is_granted('DOWNLOAD', license.certificate) %}
<a href="{{ path('app_common_download', { 'uuid': license.certificate.uuid }) }}"
target="_blank"
title="Download Nachweis">
{{ icon('download', 'w-4 h-4') }}
</a>
{% endif %}
</div>
{% else %}
{{ icon('minus', 'w-4 h-4') }}
{% endif %}
</div>
</li>
{% endfor %}
</ul>
<div class="grid grid-cols-2 gap-4">
<div>
<h2 class="text-lg font-bold pb-2">
Mögliche Jobprofile
</h2>
<ul class="list-disc pl-4">
{% for profile in teamer.jobProfiles %}
<li>
{{ profile.name }}
</li>
{% endfor %}
</ul>
</div>
<div>
<h2 class="text-lg font-bold pb-2">
Sonstiges
</h2>
<div class="flex flex-col space-y-3">
<div>
<div class="font-bold">
Status:
</div>
<div>
{{ teamer.status|teamer_status_label }}
</div>
</div>
<div>
<div class="font-bold">
Wünsche:
</div>
<div>
{{ teamer.remarks|nl2br|default('-') }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,9 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.date) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
+1 -1
View File
@@ -14,7 +14,7 @@
{{ encore_entry_script_tags('app') }} {{ encore_entry_script_tags('app') }}
{% endblock %} {% endblock %}
</head> </head>
<body class="bg-white text-gray-700 font-sans antialiased"> <body class="bg-white text-gray-700 font-sans antialiased text-sm">
{% block body %}{% endblock %} {% block body %}{% endblock %}
{% include '_partials/_flashes.html.twig' %} {% include '_partials/_flashes.html.twig' %}
</body> </body>
+11
View File
@@ -3,4 +3,15 @@
{% block title %}Mein Dashboard{% endblock %} {% block title %}Mein Dashboard{% endblock %}
{% block content %} {% block content %}
{% if errors|length %}
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-8">
<ul>
{% for error in errors %}
<li>
{{ error.message }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %} {% endblock %}
+20 -21
View File
@@ -20,9 +20,6 @@
<div {{ stimulus_controller('tabs', {}, { 'buttonActive': 'bg-gray-50' }) }}> <div {{ stimulus_controller('tabs', {}, { 'buttonActive': 'bg-gray-50' }) }}>
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-16"> <div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-16">
<div> <div>
<h1 class="text-2xl font-bold pb-8">
Mein Profil
</h1>
<ul role="list" class="space-y-1"> <ul role="list" class="space-y-1">
<li> <li>
<button type="button" class="hover:bg-gray-50 block rounded-md py-2 pr-2 pl-10 w-full text-left text-sm leading-6 font-semibold text-gray-700" {{ stimulus_target('tabs', 'button') }} {{ stimulus_action('tabs', 'select', null, { 'tab': 0 }) }}> <button type="button" class="hover:bg-gray-50 block rounded-md py-2 pr-2 pl-10 w-full text-left text-sm leading-6 font-semibold text-gray-700" {{ stimulus_target('tabs', 'button') }} {{ stimulus_action('tabs', 'select', null, { 'tab': 0 }) }}>
@@ -52,6 +49,9 @@
</ul> </ul>
</div> </div>
<div class="lg:col-span-2"> <div class="lg:col-span-2">
<h1 class="text-2xl font-bold pb-8">
Mein Profil
</h1>
{% if not form.vars.valid %} {% if not form.vars.valid %}
<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">
{% include '_partials/_form_errors.html.twig' with { 'form': form } %} {% include '_partials/_form_errors.html.twig' with { 'form': form } %}
@@ -59,7 +59,7 @@
{% include '_partials/_form_errors.html.twig' with { 'form': form.communication } %} {% include '_partials/_form_errors.html.twig' with { 'form': form.communication } %}
{% include '_partials/_form_errors.html.twig' with { 'form': form.bankAccount } %} {% include '_partials/_form_errors.html.twig' with { 'form': form.bankAccount } %}
</div> </div>
{% elseif errors|length > 0 %} {% elseif 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>
{% for error in errors %} {% for error in errors %}
@@ -121,27 +121,26 @@
<h2 class="text-xl font-bold pb-2"> <h2 class="text-xl font-bold pb-2">
Sonstiges Sonstiges
</h2> </h2>
<div class="flex flex-col space-y-4 pb-8"> <div class="flex flex-col space-y-4">
{{ form_row(form.taxId) }} {{ form_row(form.taxId) }}
{{ form_row(form.healthInsuranceCompany) }} {{ form_row(form.healthInsuranceCompany) }}
{{ form_row(form.language) }} {{ form_row(form.language) }}
{{ form_row(form.size) }} {{ form_row(form.size) }}
</div> <div {{ stimulus_controller('form-collection', { 'prototype': _self.collectionRow(form.pickups.vars.prototype)|json_encode }) }}>
<h4 class="font-bold pb-2">
<div {{ stimulus_controller('form-collection', { 'prototype': _self.collectionRow(form.pickups.vars.prototype)|json_encode }) }}> Buszustiege
<h4 class="font-bold"> </h4>
Buszustiege <div {{ stimulus_target('form-collection', 'fields')}} class="flex flex-col space-y-4 mb-4">
</h4> {% do form.pickups.setRendered %}
<div {{ stimulus_target('form-collection', 'fields')}} class="flex flex-col space-y-4 mb-4"> {%- for pickup in form.pickups -%}{{- _self.collectionRow(pickup) -}}{%- endfor -%}
{% do form.pickups.setRendered %} </div>
{%- for pickup in form.pickups -%}{{- _self.collectionRow(pickup) -}}{%- endfor -%} <div class="flex justify-end">
</div> <button type="button"
<div class="flex justify-end"> {{ stimulus_action('form-collection', 'addItem') }}
<button type="button" title="Zustieg hinzufügen">
{{ stimulus_action('form-collection', 'addItem') }} {{ icon('plus', 'w-4 h-4 pointer-events-none') }}
title="Zustieg hinzufügen"> </button>
{{ icon('plus', 'w-4 h-4 pointer-events-none') }} </div>
</button>
</div> </div>
</div> </div>
</div> </div>
+10 -4
View File
@@ -3,6 +3,9 @@
{% block title %}Meine Jobprofile & Skills{% endblock %} {% block title %}Meine Jobprofile & Skills{% endblock %}
{% block content %} {% block content %}
<h1 class="text-2xl font-bold pb-4">
Meine Jobprofile &amp; Skills
</h1>
<h2 class="text-xl font-bold pb-2"> <h2 class="text-xl font-bold pb-2">
Ausbildung Ausbildung
</h2> </h2>
@@ -26,16 +29,19 @@
{% for license in teamer.licenses %} {% for license in teamer.licenses %}
<li> <li>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
{{ icon(license.type, 'w-5 h-5') }} <span>{{ license.type|license_label }}</span>
<span>{{ license.date|date('m/Y') }}</span> <span>{{ license.date|date('m/Y') }}</span>
{% if is_granted('DOWNLOAD', license.certificate) %} {% if is_granted('DOWNLOAD', license.certificate) %}
<a href="{{ path('app_common_download', { 'uuid': license.certificate.uuid }) }}" target="_blank"> <a href="{{ path('app_common_download', { 'uuid': license.certificate.uuid }) }}"
target="_blank"
title="Download Nachweis">
{{ icon('download', 'w-4 h-4') }} {{ icon('download', 'w-4 h-4') }}
</a> </a>
{% endif %} {% endif %}
{% if is_granted('DELETE', license) %} {% if is_granted('DELETE', license) %}
<button type="button" <button type="button"
class="text-red-500" class="text-red-500"
title="Lizenz löschen"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }} {{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, { {{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?', 'title': 'Bist du sicher?',
@@ -48,9 +54,9 @@
</div> </div>
</li> </li>
{% else %} {% else %}
<div class="text-sm"> <li class="text-sm">
Du hast bisher keine Lizenzen hinterlegt Du hast bisher keine Lizenzen hinterlegt
</div> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<button type="button" <button type="button"