WIP: Implement admin dashboard
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
.upload-status-badge {
|
||||
@apply inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium;
|
||||
@apply inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap;
|
||||
}
|
||||
|
||||
.upload-status-badge--default {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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 Version20231022142623 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 assignment ADD owner_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE assignment ADD CONSTRAINT FK_30C544BA7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)');
|
||||
$this->addSql('CREATE INDEX IDX_30C544BA7E3C61F9 ON assignment (owner_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE assignment DROP FOREIGN KEY FK_30C544BA7E3C61F9');
|
||||
$this->addSql('DROP INDEX IDX_30C544BA7E3C61F9 ON assignment');
|
||||
$this->addSql('ALTER TABLE assignment DROP owner_id');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller\Admin\Assignment;
|
||||
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\User;
|
||||
use App\Form\AssignmentType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -24,7 +25,11 @@ class CreateController extends AbstractController
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$assignment = new Assignment();
|
||||
$assignment->setOwner($user);
|
||||
|
||||
$form = $this->createForm(AssignmentType::class, $assignment);
|
||||
$form->handleRequest($request);
|
||||
|
||||
|
||||
@@ -60,6 +60,16 @@ class CheckController extends AbstractController
|
||||
case Upload::STATUS_REJECTED:
|
||||
$this->rejectDocument($document, $formData->getComment());
|
||||
$this->eventDispatcher->dispatch(new DocumentRejectedEvent($formData), DocumentRejectedEvent::NAME);
|
||||
default:
|
||||
$document->setStatus($formData->getStatus());
|
||||
$this->entityManager->flush();
|
||||
$this->addFlash('success', 'Der Status wurde aktualisiert');
|
||||
$this->logger->info('Update document status', [
|
||||
'document_id' => $document->getId(),
|
||||
'document_filename' => $document->getOriginalFilename(),
|
||||
'owner' => $document->getOwner()->getFullName(),
|
||||
'status' => $formData->getStatus(),
|
||||
]);
|
||||
}
|
||||
|
||||
$returnUrl = $this->generateUrl('app_admin_document_index');
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Application;
|
||||
use App\Entity\Upload;
|
||||
use App\Repository\ApplicationRepository;
|
||||
use App\Repository\AssignmentRepository;
|
||||
use App\Repository\AvailabilityRepository;
|
||||
use App\Repository\UploadRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -9,10 +15,39 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApplicationRepository $applicationRepository,
|
||||
private readonly AssignmentRepository $assignmentRepository,
|
||||
private readonly AvailabilityRepository $availabilityRepository,
|
||||
private readonly UploadRepository $uploadRepository
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin', name: 'app_admin_index')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('admin/index.html.twig');
|
||||
$applications = $this->applicationRepository->getNew();
|
||||
$newApplicationsCount = $this->applicationRepository->getCountByStatus(Application::STATUS_NEW);
|
||||
$pendingApplicationsCount = $this->applicationRepository->getCountByStatus(Application::STATUS_PENDING);
|
||||
|
||||
$assignments = $this->assignmentRepository->getNew();
|
||||
|
||||
$availabilities = $this->availabilityRepository->getNew();
|
||||
|
||||
$documents = $this->uploadRepository->getNew();
|
||||
$newDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_NEW);
|
||||
$pendingDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_PENDING);
|
||||
|
||||
return $this->render('admin/index.html.twig', [
|
||||
'applications' => $applications,
|
||||
'newApplicationsCount' => $newApplicationsCount,
|
||||
'pendingApplicationsCount' => $pendingApplicationsCount,
|
||||
'assignments' => $assignments,
|
||||
'availabilities' => $availabilities,
|
||||
'documents' => $documents,
|
||||
'newDocumentsCount' => $newDocumentsCount,
|
||||
'pendingDocumentsCount' => $pendingDocumentsCount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,9 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
#[ORM\ManyToMany(targetEntity: Upload::class)]
|
||||
private Collection $documents;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?User $owner = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->uuid = Uuid::v4();
|
||||
@@ -421,4 +424,16 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwner(): ?User
|
||||
{
|
||||
return $this->owner;
|
||||
}
|
||||
|
||||
public function setOwner(?User $owner): static
|
||||
{
|
||||
$this->owner = $owner;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
public const TYPE_DOCUMENT = 'document';
|
||||
|
||||
public const STATUS_NEW = 'new';
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_CHECKED = 'checked';
|
||||
public const STATUS_PAID = 'paid';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
|
||||
@@ -36,10 +36,12 @@ class DocumentCheckType extends AbstractType
|
||||
'data_class' => DocumentCheckDto::class,
|
||||
'status_choices' => [
|
||||
Upload::TYPE_CONTRACT => [
|
||||
'in Bearbeitung' => Upload::STATUS_PENDING,
|
||||
'bestätigt' => Upload::STATUS_CHECKED,
|
||||
'abgelehnt' => Upload::STATUS_REJECTED,
|
||||
],
|
||||
Upload::TYPE_INVOICE => [
|
||||
'in Bearbeitung' => Upload::STATUS_PENDING,
|
||||
'bezahlt' => Upload::STATUS_PAID,
|
||||
'abgelehnt' => Upload::STATUS_REJECTED,
|
||||
],
|
||||
|
||||
@@ -72,4 +72,36 @@ class ApplicationRepository extends ServiceEntityRepository
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function getNew(int $limit = 5): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('application');
|
||||
|
||||
return $qb
|
||||
->select('application', 'assignment', 'destination', 'job_profile', 'teamer')
|
||||
->innerJoin('application.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->innerJoin('assignment.jobProfile', 'job_profile')
|
||||
->innerJoin('application.teamer', 'teamer')
|
||||
->where($qb->expr()->neq('application.status', ':status'))
|
||||
->orderBy('application.createdAt', 'DESC')
|
||||
->setParameter('status', Application::STATUS_REJECTED)
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function getCountByStatus(string $status): ?int
|
||||
{
|
||||
$qb = $this->createQueryBuilder('application');
|
||||
|
||||
return $qb
|
||||
->select($qb->expr()->count('application'))
|
||||
->where($qb->expr()->eq('application.status', ':status'))
|
||||
->setParameter('status', $status)
|
||||
->getQuery()
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,4 +121,20 @@ class AssignmentRepository extends ServiceEntityRepository
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function getNew(int $limit = 5): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('assignment');
|
||||
|
||||
return $qb
|
||||
->select('assignment', 'destination', 'job_profile', 'owner')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->innerJoin('assignment.jobProfile', 'job_profile')
|
||||
->innerJoin('assignment.owner', 'owner')
|
||||
->orderBy('assignment.createdAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,4 +65,17 @@ class AvailabilityRepository extends ServiceEntityRepository
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function getNew(int $limit = 5): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('availability');
|
||||
|
||||
return $qb
|
||||
->where($qb->expr()->isNotNull('availability.owner'))
|
||||
->orderBy('availability.createdAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\Upload;
|
||||
use App\Repository\Traits\QueryHelperTrait;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
@@ -83,4 +84,44 @@ class UploadRepository extends ServiceEntityRepository
|
||||
->getQuery()
|
||||
;
|
||||
}
|
||||
|
||||
public function getNew(int $limit = 5): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('upload');
|
||||
|
||||
return $qb
|
||||
->select('upload', 'owner', 'teamer', 'disposition', 'assignment')
|
||||
->innerJoin('upload.owner', 'owner')
|
||||
->innerJoin('owner.teamer', 'teamer')
|
||||
->leftJoin('upload.disposition', 'disposition')
|
||||
->leftJoin('disposition.assignment', 'assignment')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->in('upload.type', ':type'),
|
||||
$qb->expr()->in('upload.status', ':status')
|
||||
))
|
||||
->orderBy('upload.createdAt', 'DESC')
|
||||
->setParameter('type', [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE])
|
||||
->setParameter('status', [Upload::STATUS_NEW, Upload::STATUS_PENDING])
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function getCountByStatus(string $status): ?int
|
||||
{
|
||||
$qb = $this->createQueryBuilder('upload');
|
||||
|
||||
return $qb
|
||||
->select($qb->expr()->count('upload'))
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->in('upload.type', ':type'),
|
||||
$qb->expr()->eq('upload.status', ':status')
|
||||
))
|
||||
->setParameter('type', [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE])
|
||||
->setParameter('status', $status)
|
||||
->getQuery()
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class UploadVoter extends Voter
|
||||
if (true === in_array('ROLE_ADMINISTRATIVE', $token->getRoleNames())) {
|
||||
// Only new documents may be checked
|
||||
if (static::CHECK === $attribute) {
|
||||
return Upload::STATUS_NEW === $upload->getStatus();
|
||||
return in_array($upload->getStatus(), [Upload::STATUS_NEW, Upload::STATUS_PENDING]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,75 @@
|
||||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h2 class="font-bold text-lg">
|
||||
Neue Bewerbungen
|
||||
</h2>
|
||||
<h3 class="font-bold pb-2">
|
||||
{{ newApplicationsCount }} neu, {{ pendingApplicationsCount }} in Bearbeitung
|
||||
</h3>
|
||||
<ul>
|
||||
{% for application in applications %}
|
||||
{% set assignment = application.assignment %}
|
||||
<li>
|
||||
<a href="{{ path('app_admin_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
|
||||
{{ application.teamer.fullName(true) }} / {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} / {{ assignment.jobProfile.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-bold text-lg">
|
||||
Neue Dokumente
|
||||
</h2>
|
||||
<h3 class="font-bold pb-2">
|
||||
{{ newDocumentsCount }} neu {{ pendingDocumentsCount }} in Bearbeitung
|
||||
</h3>
|
||||
<ul>
|
||||
{% for document in documents %}
|
||||
<li>
|
||||
{% if document.disposition %}
|
||||
{% set url = path('app_admin_assignment_detail', { 'uuid': document.disposition.assignment.uuid, 'r': return_url() }) %}
|
||||
{% else %}
|
||||
{% set url = path('app_admin_teamer_profile', { 'uuid': document.owner.teamer.uuid, 'r': return_url() }) %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}">
|
||||
{{ document.owner.teamer.fullName(true) }} / {{ document.originalFilename|u.truncate(32, '...') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-bold text-lg pb-2">
|
||||
Neue Einsätze
|
||||
</h2>
|
||||
<ul>
|
||||
{% for assignment in assignments %}
|
||||
<li>
|
||||
<a href="{{ path('app_admin_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
|
||||
{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} / {{ assignment.jobProfile.name }} / {{ assignment.owner.fullName(true) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-bold text-lg pb-2">
|
||||
Neue Verfügbarkeiten
|
||||
</h2>
|
||||
<ul>
|
||||
{% for availability in availabilities %}
|
||||
<li>
|
||||
<a href="{{ path('app_admin_teamer_availability', { 'uuid': availability.owner.uuid, 'r': return_url() }) }}">
|
||||
{{ availability.owner }} / {{ availability.dateFrom|date('d.m.Y') }} - {{ availability.dateTo|date('d.m.Y') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -18,7 +18,7 @@ label:
|
||||
photo: Foto
|
||||
status:
|
||||
new: neu
|
||||
checking: in Bearbeitung
|
||||
pending: in Bearbeitung
|
||||
checked: geprüft
|
||||
paid: bezahlt
|
||||
rejected: abgelehnt
|
||||
|
||||
Reference in New Issue
Block a user