Feat: Add contacts

This commit is contained in:
Björn Fromme
2023-10-16 12:05:18 +02:00
parent f5fee787a0
commit 3fd4c6ca54
19 changed files with 658 additions and 2 deletions
+39
View File
@@ -0,0 +1,39 @@
<?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 Version20231016090702 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('CREATE TABLE contact (id INT AUTO_INCREMENT NOT NULL, photo_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, destination VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_4C62E6387E9E4C8C (photo_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE contact ADD CONSTRAINT FK_4C62E6387E9E4C8C FOREIGN KEY (photo_id) REFERENCES upload (id)');
$this->addSql('ALTER TABLE user ADD contact_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE user ADD CONSTRAINT FK_8D93D649E7A1254A FOREIGN KEY (contact_id) REFERENCES contact (id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7A1254A ON user (contact_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user DROP FOREIGN KEY FK_8D93D649E7A1254A');
$this->addSql('ALTER TABLE contact DROP FOREIGN KEY FK_4C62E6387E9E4C8C');
$this->addSql('DROP TABLE contact');
$this->addSql('DROP INDEX UNIQ_8D93D649E7A1254A ON user');
$this->addSql('ALTER TABLE user DROP contact_id');
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Controller\Admin\System\Contact;
use App\Entity\Contact;
use App\Entity\Upload;
use App\Entity\User;
use App\Form\ContactType;
use App\Model\AjaxModalResponseDto;
use App\Service\Upload\UploadHandler;
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 UploadHandler $uploadHandler,
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/contact/create', name: 'app_admin_system_contact_create')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$contact = new Contact();
$formAction = $this->generateUrl('app_admin_system_contact_create');
// Handle upload independently from form submission to avoid issues with failing validation
$uploadSession = $this->uploadHandler->getUploadSession();
if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) {
/** @var User $user */
$user = $this->getUser();
$photo = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_PHOTO);
$contact->setPhoto($photo);
$this->entityManager->flush();
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_PHOTO, $uploadSession);
$this->uploadHandler->destroyUploadSession();
}
$form = $this->createForm(
ContactType::class,
$contact,
[
'action' => $formAction,
'ajax_submit' => true,
'upload_session' => $uploadSession,
]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($contact);
$this->entityManager->flush();
$this->addFlash('success', 'Der Ansprechpartner wurde hinzugefügt');
$this->logger->info('Create contact', [
'contact' => $contact->getName(),
]);
$response->setCloseAndRedirect($this->generateUrl('app_admin_system_contact_index'));
} else {
$response->setContent($this->renderView('admin/system/contact/create.html.twig', [
'form' => $form,
]));
}
return $this->json($response);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Controller\Admin\System\Contact;
use App\Entity\Contact;
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/contact/delete/{id}', name: 'app_admin_system_contact_delete')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Contact $contact): Response
{
$this->entityManager->remove($contact);
$this->entityManager->flush();
$this->addFlash('success', 'Der Ansprechpartner wurde gelöscht');
$this->logger->info('Delete contact', [
'contact' => $contact->getName(),
]);
return $this->redirectToRoute('app_admin_system_contact_index');
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Controller\Admin\System\Contact;
use App\Entity\Contact;
use App\Entity\Upload;
use App\Entity\User;
use App\Form\ContactType;
use App\Model\AjaxModalResponseDto;
use App\Service\Upload\UploadHandler;
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 EditController extends AbstractController
{
public function __construct(
private readonly UploadHandler $uploadHandler,
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/contact/edit/{id}', name: 'app_admin_system_contact_edit')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Contact $contact, Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$formAction = $this->generateUrl('app_admin_system_contact_edit', ['id' => $contact->getId()]);
// Handle upload independently from form submission to avoid issues with failing validation
$uploadSession = $this->uploadHandler->getUploadSession();
if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) {
/** @var User $user */
$user = $this->getUser();
$photo = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_PHOTO);
if (null !== $existingPhoto = $contact->getPhoto()) {
$this->entityManager->remove($existingPhoto);
}
$contact->setPhoto($photo);
$this->entityManager->flush();
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_PHOTO, $uploadSession);
$this->uploadHandler->destroyUploadSession();
}
$form = $this->createForm(
ContactType::class,
$contact,
[
'action' => $formAction,
'ajax_submit' => true,
'upload_session' => $uploadSession,
]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush();
$this->addFlash('success', 'Der Ansprechpartner wurde aktualisiert');
$this->logger->info('Edit contact', [
'contact' => $contact->getName(),
]);
$response->setCloseAndRedirect($this->generateUrl('app_admin_system_contact_index'));
} else {
$response->setContent($this->renderView('admin/system/contact/edit.html.twig', [
'form' => $form,
]));
}
return $this->json($response);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Controller\Admin\System\Contact;
use App\Repository\ContactRepository;
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 ContactRepository $contactRepository)
{}
#[Route('/admin/system/contact', name: 'app_admin_system_contact_index')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(): Response
{
$contacts = $this
->contactRepository
->findBy([], ['name' => 'ASC'])
;
return $this->render('admin/system/contact/index.html.twig', [
'contacts' => $contacts,
]);
}
}
+18 -1
View File
@@ -2,15 +2,32 @@
namespace App\Controller\Teamer;
use App\Repository\ContactRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ContactController extends AbstractController
{
public function __construct(private readonly ContactRepository $contactRepository)
{}
#[Route('/teamer/contact', name: 'app_teamer_contact')]
public function index(): Response
{
return $this->render('teamer/contact/index.html.twig');
$generalContacts = $this
->contactRepository
->getGeneralContacts()
;
$destinationContacts = $this
->contactRepository
->getDestinationContacts()
;
return $this->render('teamer/contact/index.html.twig', [
'generalContacts' => $generalContacts,
'destinationContacts' => $destinationContacts,
]);
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace App\Entity;
use App\Repository\ContactRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ContactRepository::class)]
class Contact
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Bitte gib den Namen an')]
private ?string $name = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Bitte gib die E-Mail-Adresse an')]
#[Assert\Email(message: 'Bitte gib eine gültige E-Mail-Adresse an', mode: 'strict')]
private ?string $email = null;
#[ORM\OneToOne(cascade: ['persist', 'remove'], fetch: 'EAGER')]
#[Assert\NotNull(message: 'Bitte lade ein Foto hoch')]
private ?Upload $photo = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $destination = null;
#[ORM\OneToOne(mappedBy: 'contact', cascade: ['persist', 'remove'], fetch: 'EXTRA_LAZY')]
private ?User $user = null;
public function __toString()
{
return $this->getName();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getPhoto(): ?Upload
{
return $this->photo;
}
public function setPhoto(?Upload $photo): static
{
$this->photo = $photo;
return $this;
}
public function getDestination(): ?string
{
return $this->destination;
}
public function setDestination(?string $destination): static
{
$this->destination = $destination;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
// unset the owning side of the relation if necessary
if ($user === null && $this->user !== null) {
$this->user->setContact(null);
}
// set the owning side of the relation if necessary
if ($user !== null && $user->getContact() !== $this) {
$user->setContact($this);
}
$this->user = $user;
return $this;
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ class Teamer implements TimestampableEntityInterface
#[ORM\OneToMany(mappedBy: 'teamer', targetEntity: Disposition::class, cascade: ['remove'])]
private Collection $dispositions;
#[ORM\OneToOne(mappedBy: 'teamer')]
#[ORM\OneToOne(mappedBy: 'teamer', fetch: 'EXTRA_LAZY')]
private ?User $user = null;
#[ORM\Column]
+15
View File
@@ -45,6 +45,9 @@ class User implements UserInterface, TimestampableEntityInterface
#[ORM\OneToOne(inversedBy: 'user', cascade: ['persist', 'remove'])]
private ?Teamer $teamer = null;
#[ORM\OneToOne(inversedBy: 'user', cascade: ['persist', 'remove'])]
private ?Contact $contact = null;
public function __construct()
{
$this->uuid = Uuid::v4();
@@ -200,4 +203,16 @@ class User implements UserInterface, TimestampableEntityInterface
return $this;
}
public function getContact(): ?Contact
{
return $this->contact;
}
public function setContact(?Contact $contact): static
{
$this->contact = $contact;
return $this;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Form;
use App\Entity\Contact;
use App\Model\UploadSessionDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Name',
])
->add('email', EmailType::class, [
'label' => 'E-Mail',
])
->add('destination', TextType::class, [
'label' => 'Destination',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => Contact::class,
])
->setRequired(['upload_session'])
->setAllowedTypes('upload_session', UploadSessionDto::class)
;
}
}
+4
View File
@@ -87,6 +87,10 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'route' => 'app_admin_system_document_index',
'title' => 'Dokumente',
],
[
'route' => 'app_admin_system_contact_index',
'title' => 'Ansprechpartner',
],
],
],
];
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Repository;
use App\Entity\Contact;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Contact>
*
* @method Contact|null find($id, $lockMode = null, $lockVersion = null)
* @method Contact|null findOneBy(array $criteria, array $orderBy = null)
* @method Contact[] findAll()
* @method Contact[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ContactRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Contact::class);
}
public function getGeneralContacts(): array
{
$qb = $this->createQueryBuilder('contact');
return $qb
->leftJoin('contact.photo', 'photo')
->where($qb->expr()->isNull('contact.destination'))
->orderBy('contact.name', 'ASC')
->getQuery()
->getResult()
;
}
public function getDestinationContacts(): array
{
$qb = $this->createQueryBuilder('contact');
return $qb
->leftJoin('contact.photo', 'photo')
->where($qb->expr()->isNotNull('contact.destination'))
->orderBy('contact.name', 'ASC')
->addOrderBy('contact.destination', 'ASC')
->getQuery()
->getResult()
;
}
}
@@ -71,6 +71,7 @@
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endblock %}
@@ -0,0 +1,23 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-4">
{{ form_row(form.name) }}
{{ form_row(form.email) }}
{{ form_row(form.destination) }}
<div>
<h4 class="font-bold pb-2">
Foto
</h4>
{% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path('_uploader_upload_photo'),
'max_filesize': 5,
'max_files': 1,
'accepted_files': 'image/jpg,image/jpeg',
} %}
{{ form_errors(form) }}
</div>
</div>
<button type="submit" class="btn">
speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -0,0 +1 @@
{% include 'admin/system/contact/_form.html.twig' %}
@@ -0,0 +1 @@
{% include 'admin/system/contact/_form.html.twig' %}
@@ -0,0 +1,81 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Ansprechpartner{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Ansprechpartner
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
Name
</th>
<th>
E-Mail
</th>
<th>
Destination
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr>
<td>
{{ contact.name }}
</td>
<td>
{{ contact.email }}
</td>
<td>
{{ contact.destination|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 den Ansprechpartner wirklich löschen?',
'target-url': path('app_admin_system_contact_delete', { 'id': contact.id })
}) }}>
{{ icon('delete') }}
</button>
<button type="button"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Ansprechpartner bearbeiten',
'url': path('app_admin_system_contact_edit', { 'id': contact.id })
}) }}>
{{ icon('edit') }}
</button>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7">
Keine Daten...
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<button type="button"
class="btn"
{{ stimulus_controller('modal-button', [], [], {'ajax-modal': '#ajax-modal'}) }}
{{ stimulus_action('modal-button', 'ajax', null, {
'title': 'Ansprechpartner hinzufügen',
'url': path('app_admin_system_contact_create')
}) }}>
Neu
</button>
{% endblock %}
@@ -85,6 +85,7 @@
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
<button type="button"
+47
View File
@@ -2,8 +2,55 @@
{% block title %}Kontakt{% endblock %}
{% macro contacts(contacts, email) %}
<div class="pb-8">
<div class="flex justify-center">
<div class="flex flex-wrap items-center">
{% for contact in contacts %}
<div class="flex flex-col items-center space-y-2 px-8 py-4">
{% if contact.photo %}
<img src="{{ asset(contact.photo.filename | imagine_filter('profile')) }}"
class="w-32 h-auto border-2 border-primary rounded-full"
alt="{{ contact.name }}">
{% endif %}
<div class="font-bold">
{{ contact.name }}
</div>
{% if contact.destination %}
<div>
{{ contact.destination }}
</div>
<div>
<a href="mailto:{{ contact.email }}" class="underline">{{ contact.email }}</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% if email %}
<div class="text-center">
<a href="mailto:{{ email }}" class="text-lg fobt-bold underline">{{ email }}</a>
</div>
{% endif %}
</div>
{% endmacro %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Kontakt
</h1>
<p class="pb-4">
Hast du allgemeine Fragen zum Teamer-sein bei E&amp;P Reisen, zur Benutzung von MyE&amp;P Team, zur Abrechnung
oder Sonstigem? Hast du schon geschaut, ob deine Frage in den FAQs beantwortet wird?
</p>
<p>
Dann melde dich gerne beim Team Personalplanung!
</p>
{{ _self.contacts(generalContacts, '[email protected]') }}
<p>
Hast du Fragen zu deinem nächsten Einsatz in einer konkreten Destination? Dann melde dich gerne bei der*dem
zuständigen Reisemanager*in!
</p>
{{ _self.contacts(destinationContacts) }}
{% endblock %}