feat: news messages to be displayed on teamers' dashboards

This commit is contained in:
Björn Fromme
2024-06-23 18:37:33 +02:00
parent b58bd9f34f
commit fbad0d4e06
18 changed files with 505 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240623161718 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 news (id INT AUTO_INCREMENT NOT NULL, status SMALLINT NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', created_by VARCHAR(255) DEFAULT NULL, updated_by VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE news');
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Controller\Administrative\System\News;
use App\Entity\News;
use App\Form\NewsType;
use App\Htmx\HxRedirectResponse;
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\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class CreateController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/administrative/system/news/create', name: 'app_administrative_system_news_create')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): Response
{
$news = new News();
$form = $this->createForm(NewsType::class, $news, [
'hx_post' => $request->getUri(),
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($news);
$this->entityManager->flush();
$this->addFlash('success', 'News wurden erstellt');
$this->logger->info('Create news', [
'news_id' => $news->getId(),
]);
return new HxRedirectResponse($this->generateUrl('app_administrative_system_news_index'));
}
return $this->render('administrative/system/news/modal_create.html.twig', [
'form' => $form->createView(),
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Controller\Administrative\System\News;
use App\Entity\News;
use App\Htmx\HxRedirectResponse;
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\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DeleteController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/administrative/system/news/delete/{id}', name: 'app_administrative_system_news_delete')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(News $news, Request $request): Response
{
if (true === $request->isMethod('POST')) {
$this->entityManager->remove($news);
$this->entityManager->flush();
$this->addFlash('success', 'Die Nachricht wurde gelöscht');
$this->logger->info('Delete news', [
'news' => $news->getMessage(),
]);
return new HxRedirectResponse($this->generateUrl('app_administrative_system_news_index'));
}
return $this->render('administrative/system/news/modal_delete.html.twig', [
'news' => $news,
]);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Controller\Administrative\System\News;
use App\Entity\News;
use App\Form\NewsType;
use App\Htmx\HxRedirectResponse;
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\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class EditController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/administrative/system/news/edit/{id}', name: 'app_administrative_system_news_edit')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(News $news, Request $request): Response
{
$form = $this->createForm(NewsType::class, $news, [
'hx_post' => $request->getUri(),
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($news);
$this->entityManager->flush();
$this->addFlash('success', 'News wurden aktualisiert.');
$this->logger->info('Edit news', [
'news_id' => $news->getId(),
]);
return new HxRedirectResponse($this->generateUrl('app_administrative_system_news_index'));
}
return $this->render('administrative/system/news/modal_edit.html.twig', [
'form' => $form->createView(),
]);
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Controller\Administrative\System\News;
use App\Repository\NewsRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(private readonly NewsRepository $repository)
{
}
#[Route('/administrative/system/news', name: 'app_administrative_system_news_index')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(): Response
{
$news = $this
->repository
->findBy([], ['createdAt' => 'DESC'])
;
$activeNews = $this
->repository
->getLatestForDashboard()
;
return $this->render('administrative/system/news/index.html.twig', [
'news' => $news,
'activeNews' => $activeNews,
]);
}
}
@@ -6,6 +6,7 @@ use App\Entity\Application;
use App\Entity\Assignment; use App\Entity\Assignment;
use App\Entity\Disposition; use App\Entity\Disposition;
use App\Entity\Feedback; use App\Entity\Feedback;
use App\Entity\News;
use App\Entity\User; use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -27,6 +28,12 @@ class IndexController extends AbstractController
$user = $this->getUser(); $user = $this->getUser();
$teamer = $user->getTeamer(); $teamer = $user->getTeamer();
$news = $this
->entityManager
->getRepository(News::class)
->getLatestForDashboard()
;
$matchingAssignments = $this $matchingAssignments = $this
->entityManager ->entityManager
->getRepository(Assignment::class) ->getRepository(Assignment::class)
@@ -60,6 +67,7 @@ class IndexController extends AbstractController
$dispositionDocuments = $this->processDocuments($upcomingDispositions); $dispositionDocuments = $this->processDocuments($upcomingDispositions);
return $this->render('teamer/index.html.twig', [ return $this->render('teamer/index.html.twig', [
'news' => $news,
'teamer' => $teamer, 'teamer' => $teamer,
'matchingAssignments' => $matchingAssignments, 'matchingAssignments' => $matchingAssignments,
'recentFeedback' => $recentFeedback, 'recentFeedback' => $recentFeedback,
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\NewsRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: NewsRepository::class)]
class News implements TimestampableEntityInterface, BlameableEntityInterface
{
use TimestampableEntity;
use BlameableEntity;
public const STATUS_DEACTIVATED = 0;
public const STATUS_ACTIVE = 1;
public const STATUS_IMPORTANT = 2;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: 'smallint')]
private ?int $status;
#[ORM\Column(type: Types::TEXT)]
private ?string $message = null;
public function __construct()
{
$this->status = static::STATUS_DEACTIVATED;
}
public function getId(): ?int
{
return $this->id;
}
public function getStatus(): ?int
{
return $this->status;
}
public function setStatus(?int $status): static
{
$this->status = $status;
return $this;
}
public function getMessage(): ?string
{
return $this->message;
}
public function setMessage(?string $message): static
{
$this->message = $message;
return $this;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Form;
use App\Entity\News;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NewsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('status', ChoiceType::class, [
'label' => 'Status',
'choices' => [
'label.news.status.0' => News::STATUS_DEACTIVATED,
'label.news.status.1' => News::STATUS_ACTIVE,
'label.news.status.2' => News::STATUS_IMPORTANT,
],
])
->add('message', TextareaType::class, [
'label' => 'Nachricht',
'attr' => [
'rows' => 5,
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => News::class,
]);
}
}
+11
View File
@@ -159,6 +159,17 @@ class AdminMenuBuilder extends AbstractMenuBuilder
], ],
], ],
]); ]);
$settingsMenu->addChild('News', [
'route' => 'app_administrative_system_news_index',
'linkAttributes' => [
'title' => 'News',
],
'extras' => [
'routes' => [
['pattern' => '/^app_administrative_system_news_/']
],
],
]);
$settingsMenu->addChild('Dokumente', [ $settingsMenu->addChild('Dokumente', [
'route' => 'app_administrative_system_document_index', 'route' => 'app_administrative_system_document_index',
'linkAttributes' => [ 'linkAttributes' => [
+11
View File
@@ -94,6 +94,17 @@ class ManagerMenuBuilder extends AbstractMenuBuilder
], ],
], ],
]); ]);
$settingsMenu->addChild('News', [
'route' => 'app_administrative_system_news_index',
'linkAttributes' => [
'title' => 'News',
],
'extras' => [
'routes' => [
['pattern' => '/^app_administrative_system_news_/']
],
],
]);
$settingsMenu->addChild('Dokumente', [ $settingsMenu->addChild('Dokumente', [
'route' => 'app_administrative_system_document_index', 'route' => 'app_administrative_system_document_index',
'linkAttributes' => [ 'linkAttributes' => [
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Repository;
use App\Entity\News;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<News>
*/
class NewsRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, News::class);
}
public function getLatestForDashboard(): ?News
{
$qb = $this->createQueryBuilder('news');
return $qb
->where($qb->expr()->neq('news.status', ':status'))
->orderBy('news.status', 'DESC')
->addOrderBy('news.createdAt', 'DESC')
->setParameter('status', News::STATUS_DEACTIVATED)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult()
;
}
}
@@ -0,0 +1,10 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-4">
{{ form_row(form.status) }}
{{ form_row(form.message) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -0,0 +1,84 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}News{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
News
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
{{ icon('eye', 'w-4 h-4') }}
</th>
<th class="w-1/2">
Nachricht
</th>
<th>
Status
</th>
<th>
erstellt
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for item in news %}
<tr>
<td>
{% if item == activeNews %}
<svg viewBox="0 0 5 5" class="{{ html_classes('h-4 w-4 fill-current', { 'text-blue-600': item.status == 1, 'text-red-600': item.status == 2 }) }}">
<circle cx="3" cy="3" r="2" />
</svg>
{% endif %}
</td>
<td>
{{ item.message | u.truncate(48, '...') }}
</td>
<td>
{{ ('label.news.status.' ~ item.status) | trans }}
</td>
<td>
{{ item.createdAt | date('d.m.Y') }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
<button type="button"
hx-get="{{ path('app_administrative_system_news_edit', { 'id': item.id }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('edit') }}
</button>
<button type="button"
class="text-red-500"
hx-get="{{ path('app_administrative_system_news_delete', { 'id': item.id }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('delete') }}
</button>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">
Keine Daten...
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<button type="button"
class="btn"
hx-get="{{ path('app_administrative_system_news_create') }}"
hx-target="body"
hx-swap="beforeend">
Neu
</button>
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}News anlegen{% endblock %}
{% block content %}
{% include 'administrative/system/news/form.html.twig' %}
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block content %}
<div>
Möchtest du die News wirklich löschen?
</div>
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}News bearbeiten{% endblock %}
{% block content %}
{% include 'administrative/system/news/form.html.twig' %}
{% endblock %}
+8
View File
@@ -11,6 +11,14 @@
{% if not teamer.photo %} {% if not teamer.photo %}
{% include '_partials/_toast.html.twig' with { level: 'info', messages: ['Bitte lade ein Foto von dir hoch'] } %} {% include '_partials/_toast.html.twig' with { level: 'info', messages: ['Bitte lade ein Foto von dir hoch'] } %}
{% endif %} {% endif %}
{% if news is not null %}
<div class="{{ html_classes('p-4 mb-4 flex items-start space-x-2 text-gray-100 rounded-md', { 'bg-blue-600': news.status == 1, 'bg-red-600': news.status == 2 }) }}">
{{ icon('alert', 'w-5 h-5 flex-shrink-0') }}
<div class="leading-relaxed text-base">
{{ news.message | nl2br }}
</div>
</div>
{% endif %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2"> <div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2">
<h2 class="font-bold text-lg pb-2"> <h2 class="font-bold text-lg pb-2">
+5
View File
@@ -29,6 +29,11 @@ label:
hr: Personalplanung hr: Personalplanung
management: Reisemanagement management: Reisemanagement
house_management: Hausmanagement house_management: Hausmanagement
news:
status:
0: deaktiviert
1: aktiv
2: wichtig
message: message:
confirm_contract: confirm_contract: