WIP: Implement profile editing
This commit is contained in:
@@ -6,6 +6,12 @@ oneup_uploader:
|
||||
namer: app.upload_namer
|
||||
storage:
|
||||
directory: '%kernel.project_dir%/uploads/photo'
|
||||
certificate:
|
||||
frontend: dropzone
|
||||
use_orphanage: true
|
||||
namer: app.upload_namer
|
||||
storage:
|
||||
directory: '%kernel.project_dir%/uploads/certificate'
|
||||
chunks:
|
||||
maxage: 86400
|
||||
storage:
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\BusProNet\DataProvider;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\ApiClientException;
|
||||
use App\BusProNet\Model\Country;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
@@ -30,4 +31,9 @@ class Countries
|
||||
|
||||
return $countries;
|
||||
}
|
||||
|
||||
public function get(int $id): ?Country
|
||||
{
|
||||
return $this->getAll()[$id] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\BusProNet\DataProvider;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\ApiClientException;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
@@ -29,6 +30,10 @@ class Pickups
|
||||
}
|
||||
|
||||
return $pickups;
|
||||
}
|
||||
|
||||
public function get(int $busProId): ?Pickup
|
||||
{
|
||||
return $this->getAll()[$busProId] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -176,14 +176,15 @@ class ResponseParser
|
||||
$countries = [];
|
||||
|
||||
foreach ($xml->xpath('laender/land') as $item) {
|
||||
$id = (int) $item->attributes()['id'];
|
||||
$country = new Country();
|
||||
$country
|
||||
->setId((int)$item->attributes()['id'])
|
||||
->setName((string)$item->attributes()['bezeichnung'])
|
||||
->setToken((string)$item->attributes()['kuerzel'])
|
||||
->setNationality((string)$item->attributes()['nationalitaet'])
|
||||
->setId($id)
|
||||
->setName((string) $item->attributes()['bezeichnung'])
|
||||
->setToken((string) $item->attributes()['kuerzel'])
|
||||
->setNationality((string) $item->attributes()['nationalitaet'])
|
||||
;
|
||||
$countries[] = $country;
|
||||
$countries[$id] = $country;
|
||||
}
|
||||
|
||||
$response = new BaseDataResponse();
|
||||
@@ -197,15 +198,17 @@ class ResponseParser
|
||||
$pickups = [];
|
||||
|
||||
foreach ($xml->xpath('zustieg') as $item) {
|
||||
$id = (int) $item->attributes()['id'];
|
||||
$busProId = (int) $item->attributes()['idbuspro'];
|
||||
$pickup = new Pickup();
|
||||
$pickup
|
||||
->setId((int)$item->attributes()['id'])
|
||||
->setBusProId((int)$item->attributes()['idbuspro'])
|
||||
->setCode((string)$item->attributes()['code'])
|
||||
->setCity((string)$item->xpath('ort')[0])
|
||||
->setStreet((string)$item->xpath('strasse')[0])
|
||||
->setId($id)
|
||||
->setBusProId($busProId)
|
||||
->setCode((string) $item->attributes()['code'])
|
||||
->setCity((string) $item->xpath('ort')[0])
|
||||
->setStreet((string) $item->xpath('strasse')[0])
|
||||
;
|
||||
$pickups[] = $pickup;
|
||||
$pickups[$busProId] = $pickup;
|
||||
}
|
||||
|
||||
$response = new BaseDataResponse();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Common;
|
||||
|
||||
use App\Entity\Upload;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
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 DownloadController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly UploadHandler $uploadHandler)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/download/{uuid}', name: 'app_common_download')]
|
||||
#[IsGranted('DOWNLOAD', subject: 'upload')]
|
||||
public function index(Upload $upload): Response
|
||||
{
|
||||
$path = $this->uploadHandler->getUploadFilepath($upload);
|
||||
$originalFilename = $upload->getOriginalFilename();
|
||||
|
||||
return $this->file($path, $originalFilename);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class IndexController extends AbstractController
|
||||
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_PHOTO);
|
||||
$teamer->setPhoto($upload);
|
||||
$this->entityManager->flush();
|
||||
$this->uploadHandler->moveUploadSessionFilesFromOrphanage('photo', $uploadSession);
|
||||
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_PHOTO, $uploadSession);
|
||||
$this->uploadHandler->destroyUploadSession();
|
||||
|
||||
$this->addFlash('success', 'Dein Profilbild wurde aktualisiert');
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Teamer\Profile\License;
|
||||
|
||||
use App\Entity\License;
|
||||
use App\Entity\Upload;
|
||||
use App\Entity\User;
|
||||
use App\Form\LicenseType;
|
||||
use App\Model\AjaxModalResponseDto;
|
||||
use App\Model\UploadSessionDto;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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 AddController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UploadHandler $uploadHandler
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/teamer/profile/license/add', name: 'app_teamer_profile_license_add')]
|
||||
#[IsGranted('ROLE_TEAMER')]
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$response = new AjaxModalResponseDto();
|
||||
$action = $this->generateUrl('app_teamer_profile_license_add');
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$license = new License();
|
||||
|
||||
// Handle upload independently from form submission to avoid issues with failing validation
|
||||
$uploadSession = $this->uploadHandler->getUploadSession();
|
||||
if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) {
|
||||
$this->updateCertificate($user, $license, $uploadSession);
|
||||
}
|
||||
|
||||
$form = $this->createForm(LicenseType::class, $license, ['action' => $action, 'ajax_submit' => true, 'upload_session' => $uploadSession]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$teamer = $user->getTeamer();
|
||||
|
||||
$teamer->addLicense($license);
|
||||
|
||||
$this->entityManager->persist($license);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$redirectUrl = $this->generateUrl('app_teamer_profile_skills');
|
||||
$response->setCloseAndRedirect($redirectUrl);
|
||||
} else {
|
||||
$content = $this->renderView('teamer/profile/license/add.html.twig', [
|
||||
'form' => $form,
|
||||
]);
|
||||
$response->setContent($content);
|
||||
}
|
||||
|
||||
return $this->json($response);
|
||||
}
|
||||
|
||||
private function updateCertificate(User $user, License $license, UploadSessionDto $uploadSession): void
|
||||
{
|
||||
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_CERTIFICATE);
|
||||
$license->setCertificate($upload);
|
||||
$this->entityManager->flush();
|
||||
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_CERTIFICATE, $uploadSession);
|
||||
$this->uploadHandler->destroyUploadSession();
|
||||
|
||||
$this->addFlash('success', 'Der Nachweis wurde hochgeladen');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Controller\Teamer\Profile;
|
||||
|
||||
use App\Entity\User;
|
||||
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,8 +13,14 @@ class SkillsController extends AbstractController
|
||||
{
|
||||
#[Route('/teamer/profile/skills', name: 'app_teamer_profile_skills')]
|
||||
#[IsGranted('ROLE_TEAMER')]
|
||||
public function index(): Response
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return $this->render('teamer/profile/skills.html.twig');
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$teamer = $user->getTeamer();
|
||||
|
||||
return $this->render('teamer/profile/skills.html.twig', [
|
||||
'teamer' => $teamer,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Repository\LicenseRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: LicenseRepository::class)]
|
||||
class License implements TimestampableEntityInterface
|
||||
@@ -25,12 +26,14 @@ class License implements TimestampableEntityInterface
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(length: 64)]
|
||||
private ?string $type = null;
|
||||
private ?string $type;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[Assert\NotNull(message: 'Bitte gib das Datum an')]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||
#[Assert\NotNull(message: 'Bitte lade den Nachweis hoch')]
|
||||
private ?Upload $certificate = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'licenses')]
|
||||
|
||||
@@ -16,6 +16,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
use TimestampableEntity;
|
||||
|
||||
public const TYPE_PHOTO = 'photo';
|
||||
public const TYPE_CERTIFICATE = 'certificate';
|
||||
|
||||
public const STATUS_NEW = 'new';
|
||||
public const STATUS_IN_PROCESS = 'in_process';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\DataTransformer\FirstDayOfMonthDateTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AbbreviatedDateType extends AbstractType
|
||||
{
|
||||
public function getParent(): string
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addViewTransformer(new FirstDayOfMonthDateTransformer());
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'input' => 'datetime_immutable',
|
||||
'format' => 'y-MMMM-d',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class BpnPickupType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choice_loader' => new BpnPickupsChoiceLoader($this->pickups),
|
||||
'placeholder' => 'Bitte wählen...',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\DataTransformer;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
class EntityToIdTransformer implements DataTransformerInterface
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly string $class)
|
||||
{
|
||||
}
|
||||
|
||||
public function transform($value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value->getId();
|
||||
}
|
||||
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getRepository($this->class)->find($value);
|
||||
|
||||
if (null === $entity) {
|
||||
throw new TransformationFailedException(sprintf('No %s with id %d found', $this->class, $value));
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\DataTransformer;
|
||||
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
|
||||
class FirstDayOfMonthDateTransformer implements DataTransformerInterface
|
||||
{
|
||||
public function transform($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (empty($value['year'])) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$value['month'] = $value['month'] ?? 1;
|
||||
$value['day'] = 1;
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class DatepickerType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'html5' => false,
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
'min_date' => null,
|
||||
'max_date' => null,
|
||||
'disable_weekends' => false,
|
||||
]);
|
||||
$resolver->setAllowedTypes('min_date', ['null', \DateTimeImmutable::class]);
|
||||
$resolver->setAllowedTypes('max_date', ['null', \DateTimeImmutable::class]);
|
||||
$resolver->setAllowedTypes('disable_weekends', 'bool');
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['min_date'] = $options['min_date'];
|
||||
$view->vars['max_date'] = $options['max_date'];
|
||||
$view->vars['disable_weekends'] = $options['disable_weekends'];
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\DataTransformer\EntityToIdTransformer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class HiddenEntityType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$transformer = new EntityToIdTransformer($this->entityManager, $options['class']);
|
||||
$builder->addModelTransformer($transformer);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['entity'] = $form->getData();
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setRequired(['class'])
|
||||
->setDefaults([
|
||||
'invalid_message' => 'The entity does not exist.',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return HiddenType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\License;
|
||||
use App\Model\UploadSessionDto;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class LicenseType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('type', ChoiceType::class, [
|
||||
'label' => 'Art der Lizenz',
|
||||
'choices' => [
|
||||
'Skilehrer*in' => License::TYPE_SKI,
|
||||
'Snowboardlehrer*in' => License::TYPE_SNOWBOARD,
|
||||
],
|
||||
])
|
||||
->add('date', AbbreviatedDateType::class, [
|
||||
'label' => 'Datum',
|
||||
'years' => range((int) date('Y'), (int) date('Y') - 15)
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
/** @var UploadSessionDto $uploadSession */
|
||||
$uploadSession = $options['upload_session'];
|
||||
$view->vars['upload_session_params'] = [
|
||||
UploadHandler::SESSION_KEY => $uploadSession->getUid(),
|
||||
];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => License::class,
|
||||
])
|
||||
->setRequired(['upload_session'])
|
||||
->setAllowedTypes('upload_session', UploadSessionDto::class)
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class AjaxModalResponseDto
|
||||
{
|
||||
private string $content;
|
||||
private string $redirect;
|
||||
private bool $close = false;
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): self
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRedirect(): ?string
|
||||
{
|
||||
return $this->redirect;
|
||||
}
|
||||
|
||||
public function setRedirect(string $redirect): self
|
||||
{
|
||||
$this->redirect = $redirect;
|
||||
$this->close = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCloseAndRedirect(string $redirect): self
|
||||
{
|
||||
$this->redirect = $redirect;
|
||||
$this->close = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isClose(): bool
|
||||
{
|
||||
return $this->close;
|
||||
}
|
||||
|
||||
public function setClose(bool $close): void
|
||||
{
|
||||
$this->close = $close;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\Upload;
|
||||
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 UploadVoter extends Voter
|
||||
{
|
||||
public const VIEW = 'VIEW';
|
||||
public const DELETE = 'DELETE';
|
||||
public const DOWNLOAD = 'DOWNLOAD';
|
||||
|
||||
public function __construct(private readonly Security $security)
|
||||
{}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (! $subject instanceof Upload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($attribute, [static::VIEW, static::DELETE, static::DOWNLOAD]);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var Upload $upload */
|
||||
$upload = $subject;
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
return $upload->getOwner() === $user;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 px-4 shadow w-full max-w-xl">
|
||||
<div class="bg-white p-8">
|
||||
<div class="flex justify-between pb-4">
|
||||
<div class="text-lg font-medium" {{ stimulus_target('ajax-modal', 'title') }}></div>
|
||||
<div class="text-2xl font-bold" {{ stimulus_target('ajax-modal', 'title') }}></div>
|
||||
<button type="button"
|
||||
class="inline-block"
|
||||
{{ stimulus_action('ajax-modal', 'hide') }}>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<ul>
|
||||
{% for child in form.children %}
|
||||
{% for error in child.vars.errors %}
|
||||
<li>
|
||||
{{ error.message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{%endfor%}
|
||||
{% for error in form.vars.errors %}
|
||||
<li>
|
||||
{{ error.message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -36,6 +36,8 @@
|
||||
<div class="flex items-center space-x-2">
|
||||
{% for license in jobProfile.requiredLicenses %}
|
||||
{{ icon(license, 'w-5 h-5') }}
|
||||
{% else %}
|
||||
-
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{%- endblock form_row -%}
|
||||
|
||||
{%- block form_label -%}
|
||||
{% set class = ' font-bold' %}
|
||||
{% set class = 'block font-bold pb-2' %}
|
||||
{% if errors|length %}
|
||||
{% set class = class ~ ' text-red-500' %}
|
||||
{% endif %}
|
||||
@@ -35,7 +35,7 @@
|
||||
{%- block form_widget_simple -%}
|
||||
{%- set type = type|default('text') -%}
|
||||
{%- if type != 'hidden' -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' mt-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
@@ -52,7 +52,7 @@
|
||||
{%- endblock form_widget_simple -%}
|
||||
|
||||
{%- block textarea_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' mt-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
@@ -68,7 +68,7 @@
|
||||
{%- endblock textarea_widget -%}
|
||||
|
||||
{%- block choice_widget_collapsed -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' mt-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
@@ -115,6 +115,19 @@
|
||||
</div>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block abbreviated_date_widget %}
|
||||
{%- set class = 'flex items-center space-x-1 ' ~ attr.class|default('') -%}
|
||||
{%- do form.setRendered -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{%- endif -%}
|
||||
<div class="{{ class }}">
|
||||
{{ form_widget(form.month, { 'attr': attr })}}
|
||||
<span>/</span>
|
||||
{{ form_widget(form.year, { 'attr': attr })}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{%- block money_widget -%}
|
||||
{% set currency_class = 'absolute top-1/2 transform -translate-y-1/2 right-0 mr-2' %}
|
||||
{% if errors|length %}
|
||||
@@ -127,3 +140,24 @@
|
||||
<span class="{{ currency_class }}">€</span>
|
||||
</div>
|
||||
{%- endblock money_widget -%}
|
||||
|
||||
{%- block datepicker_widget -%}
|
||||
{%- set minDate = form.vars.min_date ? form.vars.min_date | date('Y-m-d') : null -%}
|
||||
{%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
<div {{ stimulus_controller('datepicker', { 'minDate': minDate, 'maxDate': maxDate, 'disableWeekends': form.vars.disable_weekends }) }}>
|
||||
<input type="text" name="{{ form.vars.full_name }}" value="{{ value }}" {{ block('widget_attributes') }}
|
||||
{{ stimulus_target('datepicker', 'field') }} />
|
||||
</div>
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock datepicker_widget %}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<div class="flex items-center space-x-4">
|
||||
{% if app.user.teamer.photo %}
|
||||
<a href="{{ path('app_teamer_profile_index') }}" title="Mein Profil">
|
||||
<img src="{{ asset(teamer.photo.filename | imagine_filter('profile')) }}"
|
||||
<img src="{{ asset(app.user.teamer.photo.filename | imagine_filter('profile')) }}"
|
||||
class="h-8 w-auto rounded-full border-2 border-primary"
|
||||
alt="{{ teamer.firstName }}">
|
||||
alt="{{ app.user.teamer.firstName }}">
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_security_logout') }}" class="hidden lg:block">
|
||||
|
||||
@@ -2,23 +2,6 @@
|
||||
|
||||
{% block title %}Mein Profil{% endblock %}
|
||||
|
||||
{% macro formErrors(form) %}
|
||||
<ul>
|
||||
{% for child in form.children %}
|
||||
{% for error in child.vars.errors %}
|
||||
<li>
|
||||
{{ error.message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{%endfor%}
|
||||
{% for error in form.vars.errors %}
|
||||
<li>
|
||||
{{ error.message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro collectionRow(form) %}
|
||||
<div {{ stimulus_target('form-collection', 'field') }}>
|
||||
<div class="flex items-center space-x-4">
|
||||
@@ -71,10 +54,10 @@
|
||||
<div class="lg:col-span-2">
|
||||
{% if not form.vars.valid %}
|
||||
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-8">
|
||||
{{ _self.formErrors(form) }}
|
||||
{{ _self.formErrors(form.address) }}
|
||||
{{ _self.formErrors(form.communication) }}
|
||||
{{ _self.formErrors(form.bankAccount) }}
|
||||
{% include '_partials/_form_errors.html.twig' with { 'form': form } %}
|
||||
{% include '_partials/_form_errors.html.twig' with { 'form': form.address } %}
|
||||
{% include '_partials/_form_errors.html.twig' with { 'form': form.communication } %}
|
||||
{% include '_partials/_form_errors.html.twig' with { 'form': form.bankAccount } %}
|
||||
</div>
|
||||
{% elseif errors|length > 0 %}
|
||||
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-8">
|
||||
@@ -183,6 +166,7 @@
|
||||
{% include '_partials/_upload_collection_form.html.twig' with {
|
||||
'endpoint_upload': path('_uploader_upload_photo'),
|
||||
'upload_session_params': form.vars.upload_session_params,
|
||||
'accepted_files': 'image/jpg,image/jpeg',
|
||||
} %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{{ form_start(form) }}
|
||||
{% if not form.vars.valid %}
|
||||
<div class="border border-red-500 rounded-md p-4 text-red-500 mb-4">
|
||||
{% include '_partials/_form_errors.html.twig' with { 'form': form } %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex flex-col space-y-4 pb-8">
|
||||
{{ form_row(form.type) }}
|
||||
{{ form_row(form.date) }}
|
||||
<div>
|
||||
<label class="block font-bold pb-2">
|
||||
Nachweis hochladen (PDF)
|
||||
</label>
|
||||
{% include '_partials/_upload_collection_form.html.twig' with {
|
||||
'endpoint_upload': path('_uploader_upload_certificate'),
|
||||
'upload_session_params': form.vars.upload_session_params,
|
||||
'accepted_files': 'application/pdf',
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">
|
||||
Speichern
|
||||
</button>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
@@ -3,4 +3,33 @@
|
||||
{% block title %}Meine Jobprofile & Skills{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="text-xl font-bold pb-2">
|
||||
Lizenzen
|
||||
</h2>
|
||||
<div class="flex flex-col space-x-4 pb-8">
|
||||
{% for license in teamer.licenses %}
|
||||
<div class="flex items-center space-x-2">
|
||||
{{ icon(license.type, 'w-5 h-5') }}
|
||||
<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">
|
||||
{{ icon('download', 'w-4 h-4') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-sm pb-8">
|
||||
Du hast bisher keine Lizenzen hinterlegt
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
|
||||
{{ stimulus_action('modal-button', 'ajax', null, {
|
||||
'title': 'Lizenz hinzufügen',
|
||||
'url': path('app_teamer_profile_license_add')
|
||||
}) }}>
|
||||
Lizenz hinzufügen
|
||||
</button>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user