feat: dedicated dashboard widgets for contracts and invoices

This commit is contained in:
Björn Fromme
2025-01-15 16:17:55 +01:00
parent 6af42e5703
commit b62cf18ede
5 changed files with 194 additions and 97 deletions
+12 -6
View File
@@ -38,9 +38,12 @@ class IndexController extends AbstractController
$availabilities = $this->entityManager->getRepository(Availability::class)->getNew(); $availabilities = $this->entityManager->getRepository(Availability::class)->getNew();
$uploadRepository = $this->entityManager->getRepository(Upload::class); $uploadRepository = $this->entityManager->getRepository(Upload::class);
$documents = $uploadRepository->getNew(); $newContracts = $uploadRepository->getNewByType(Upload::TYPE_CONTRACT);
$newDocumentsCount = $uploadRepository->getCountByStatus(Upload::STATUS_NEW); $newContractsCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_NEW, Upload::TYPE_CONTRACT);
$pendingDocumentsCount = $uploadRepository->getCountByStatus(Upload::STATUS_PENDING); $pendingContractsCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_PENDING, Upload::TYPE_CONTRACT);
$newInvoices = $uploadRepository->getNewByType(Upload::TYPE_INVOICE);
$newInvoicesCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_NEW, Upload::TYPE_INVOICE);
$pendingInvoicesCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_PENDING, Upload::TYPE_INVOICE);
$dispositionRepository = $this->entityManager->getRepository(Disposition::class); $dispositionRepository = $this->entityManager->getRepository(Disposition::class);
$overdueContracts = $dispositionRepository->findOverdueContracts(); $overdueContracts = $dispositionRepository->findOverdueContracts();
@@ -55,9 +58,12 @@ class IndexController extends AbstractController
'pendingFeedbacksCount' => $pendingFeedbacksCount, 'pendingFeedbacksCount' => $pendingFeedbacksCount,
'assignments' => $assignments, 'assignments' => $assignments,
'availabilities' => $availabilities, 'availabilities' => $availabilities,
'documents' => $documents, 'newContracts' => $newContracts,
'newDocumentsCount' => $newDocumentsCount, 'newContractsCount' => $newContractsCount,
'pendingDocumentsCount' => $pendingDocumentsCount, 'pendingContractsCount' => $pendingContractsCount,
'newInvoices' => $newInvoices,
'newInvoicesCount' => $newInvoicesCount,
'pendingInvoicesCount' => $pendingInvoicesCount,
'overdueContracts' => $overdueContracts, 'overdueContracts' => $overdueContracts,
'newDispositions' => $newDispositions, 'newDispositions' => $newDispositions,
'overdueFeedbacks' => $overdueFeedbacks, 'overdueFeedbacks' => $overdueFeedbacks,
+20 -6
View File
@@ -22,16 +22,30 @@ class IndexController extends AbstractController
{ {
$dispositions = $this->dispositionRepository->getNew(); $dispositions = $this->dispositionRepository->getNew();
$documents = $this->uploadRepository->getNew(); $newContracts = $this->uploadRepository->getNewByType(Upload::TYPE_CONTRACT);
$newDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_NEW); $newContractsCount = $this
$pendingDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_PENDING); ->uploadRepository
->getCountByStatusAndType(Upload::STATUS_NEW, Upload::TYPE_CONTRACT);
$pendingContractsCount = $this
->uploadRepository
->getCountByStatusAndType(Upload::STATUS_PENDING, Upload::TYPE_CONTRACT);
$newInvoices = $this->uploadRepository->getNewByType(Upload::TYPE_INVOICE);
$newInvoicesCount = $this
->uploadRepository
->getCountByStatusAndType(Upload::STATUS_NEW, Upload::TYPE_INVOICE);
$pendingInvoicesCount = $this
->uploadRepository
->getCountByStatusAndType(Upload::STATUS_PENDING, Upload::TYPE_INVOICE);
$overdueFeedbacks = $this->dispositionRepository->findDispositionsWithOverdueFeedback(7); $overdueFeedbacks = $this->dispositionRepository->findDispositionsWithOverdueFeedback(7);
return $this->render('manager/index.html.twig', [ return $this->render('manager/index.html.twig', [
'dispositions' => $dispositions, 'dispositions' => $dispositions,
'documents' => $documents, 'newContracts' => $newContracts,
'newDocumentsCount' => $newDocumentsCount, 'newContractsCount' => $newContractsCount,
'pendingDocumentsCount' => $pendingDocumentsCount, 'pendingContractsCount' => $pendingContractsCount,
'newInvoices' => $newInvoices,
'newInvoicesCount' => $newInvoicesCount,
'pendingInvoicesCount' => $pendingInvoicesCount,
'overdueFeedbacks' => $overdueFeedbacks, 'overdueFeedbacks' => $overdueFeedbacks,
]); ]);
} }
+6 -7
View File
@@ -147,7 +147,7 @@ class UploadRepository extends ServiceEntityRepository
return $qb->getQuery(); return $qb->getQuery();
} }
public function getNew(int $limit = 5): array public function getNewByType(string $type): array
{ {
$qb = $this->createQueryBuilder('upload'); $qb = $this->createQueryBuilder('upload');
@@ -158,29 +158,28 @@ class UploadRepository extends ServiceEntityRepository
->leftJoin('upload.disposition', 'disposition') ->leftJoin('upload.disposition', 'disposition')
->leftJoin('disposition.assignment', 'assignment') ->leftJoin('disposition.assignment', 'assignment')
->where($qb->expr()->andX( ->where($qb->expr()->andX(
$qb->expr()->in('upload.type', ':type'), $qb->expr()->eq('upload.type', ':type'),
$qb->expr()->in('upload.status', ':status') $qb->expr()->in('upload.status', ':status')
)) ))
->orderBy('upload.createdAt', 'DESC') ->orderBy('upload.createdAt', 'DESC')
->setParameter('type', [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE]) ->setParameter('type', $type)
->setParameter('status', [Upload::STATUS_NEW, Upload::STATUS_PENDING]) ->setParameter('status', [Upload::STATUS_NEW, Upload::STATUS_PENDING])
->setMaxResults($limit)
->getQuery() ->getQuery()
->getResult() ->getResult()
; ;
} }
public function getCountByStatus(string $status): ?int public function getCountByStatusAndType(string $status, string $type): ?int
{ {
$qb = $this->createQueryBuilder('upload'); $qb = $this->createQueryBuilder('upload');
return $qb return $qb
->select($qb->expr()->count('upload')) ->select($qb->expr()->count('upload'))
->where($qb->expr()->andX( ->where($qb->expr()->andX(
$qb->expr()->in('upload.type', ':type'), $qb->expr()->eq('upload.type', ':type'),
$qb->expr()->eq('upload.status', ':status') $qb->expr()->eq('upload.status', ':status')
)) ))
->setParameter('type', [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE]) ->setParameter('type', $type)
->setParameter('status', $status) ->setParameter('status', $status)
->getQuery() ->getQuery()
->getSingleScalarResult() ->getSingleScalarResult()
+62 -25
View File
@@ -53,31 +53,68 @@
</div> </div>
<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="flex justify-between pb-2"> <h2 class="flex justify-between pb-2">
<span class="font-bold text-lg">Neue Dokumente</span> <span class="font-bold text-lg">Neue Honorarverträge</span>
{% if newDocumentsCount > 0 or pendingDocumentsCount > 0 %} {% if newContractsCount > 0 or pendingContractsCount > 0 %}
<span>{{ newDocumentsCount }} neu, {{ pendingDocumentsCount }} in Bearbeitung</span> <span>{{ newContractsCount }} neu, {{ pendingContractsCount }} in Bearbeitung</span>
{% endif %} {% endif %}
</h2> </h2>
<ul class="divide-y divide-gray-200"> <div class="max-h-64 overflow-y-scroll overflow-x-hidden">
{% for document in documents %} <ul class="divide-y divide-gray-200">
<li class="py-2 first:pt-0 last:pb-0"> {% for document in newContracts %}
<button type="button" {% set assignment = document.disposition.assignment %}
class="flex items-center space-x-1 truncate" <li class="py-2 first:pt-0 last:pb-0">
hx-get="{{ path('app_administrative_document_check', { 'uuid': document.uuid, 'r': return_url() }) }}" <button type="button"
hx-target="body" class="flex items-center space-x-1 truncate"
hx-swap="beforeend"> hx-get="{{ path('app_administrative_document_check', { 'uuid': document.uuid, 'r': return_url() }) }}"
{{ icon('download', 'w-4 h-4 shrink-0') }} hx-target="body"
<span>{{ document.owner.teamer.fullName(true) }}</span> hx-swap="beforeend">
{% include '_partials/_dot.html.twig' %} {{ icon('download', 'w-4 h-4 shrink-0') }}
<span>{{ document.typeLabel }}</span> <span>{{ document.owner.teamer.fullName(true) }}</span>
</button> {% include '_partials/_dot.html.twig' %}
</li> <span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
{% else %} {% include '_partials/_dot.html.twig' %}
<li class="py-2 first:pt-0 last:pb-0"> <span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
- </button>
</li> </li>
{% endfor %} {% else %}
</ul> <li class="py-2 first:pt-0 last:pb-0">
-
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2">
<h2 class="flex justify-between pb-2">
<span class="font-bold text-lg">Neue Honorarnoten</span>
{% if newInvoicesCount > 0 or pendingInvoicesCount > 0 %}
<span>{{ newInvoicesCount }} neu, {{ pendingInvoicesCount }} in Bearbeitung</span>
{% endif %}
</h2>
<div class="max-h-64 overflow-y-scroll overflow-x-hidden">
<ul class="divide-y divide-gray-200">
{% for document in newInvoices %}
{% set assignment = document.disposition.assignment %}
<li class="py-2 first:pt-0 last:pb-0">
<button type="button"
class="flex items-center space-x-1 truncate"
hx-get="{{ path('app_administrative_document_check', { 'uuid': document.uuid, 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('download', 'w-4 h-4 shrink-0') }}
<span>{{ document.owner.teamer.fullName(true) }}</span>
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
{% include '_partials/_dot.html.twig' %}
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
</button>
</li>
{% else %}
<li class="py-2 first:pt-0 last:pb-0">
-
</li>
{% endfor %}
</ul>
</div>
</div> </div>
<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">
@@ -166,7 +203,7 @@
<span>{{ overdueFeedbacksCount }} überfälllig</span> <span>{{ overdueFeedbacksCount }} überfälllig</span>
{% endif %} {% endif %}
</h2> </h2>
<div class="max-h-64 overflow-y-scroll"> <div class="max-h-64 overflow-y-scroll overflow-x-hidden">
<ul class="divide-y divide-gray-200"> <ul class="divide-y divide-gray-200">
{% for disposition in overdueFeedbacks %} {% for disposition in overdueFeedbacks %}
{% set assignment = disposition.assignment %} {% set assignment = disposition.assignment %}
@@ -201,7 +238,7 @@
<span>{{ overdueContractsCount }} überfälllig</span> <span>{{ overdueContractsCount }} überfälllig</span>
{% endif %} {% endif %}
</h2> </h2>
<div class="max-h-64 overflow-y-scroll"> <div class="max-h-64 overflow-y-scroll overflow-x-hidden">
<ul class="divide-y divide-gray-200"> <ul class="divide-y divide-gray-200">
{% for disposition in overdueContracts %} {% for disposition in overdueContracts %}
{% set assignment = disposition.assignment %} {% set assignment = disposition.assignment %}
+94 -53
View File
@@ -3,34 +3,69 @@
{% block content %} {% block content %}
<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="flex justify-between pb-2">
Neue Dokumente <span class="font-bold text-lg">Neue Honorarverträge</span>
{% if newContractsCount > 0 or pendingContractsCount > 0 %}
<span>{{ newContractsCount }} neu, {{ pendingContractsCount }} in Bearbeitung</span>
{% endif %}
</h2> </h2>
{% if newDocumentsCount > 0 or pendingDocumentsCount > 0 %} <div class="max-h-64 overflow-y-scroll overflow-x-hidden">
<h3 class="font-bold pb-2"> <ul class="divide-y divide-gray-200">
{{ newDocumentsCount }} neu {{ pendingDocumentsCount }} in Bearbeitung {% for document in newContracts %}
</h3> {% set assignment = document.disposition.assignment %}
{% endif %} <li class="py-2 first:pt-0 last:pb-0">
<ul class="divide-y divide-gray-200"> <button type="button"
{% for document in documents %} class="flex items-center space-x-1 truncate"
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden"> hx-get="{{ path('app_administrative_document_check', { 'uuid': document.uuid, 'r': return_url() }) }}"
<button type="button" hx-target="body"
class="flex items-center space-x-1" hx-swap="beforeend">
hx-get="{{ path('app_administrative_document_check', { 'uuid': document.uuid, 'r': return_url() }) }}" {{ icon('download', 'w-4 h-4 shrink-0') }}
hx-target="body" <span>{{ document.owner.teamer.fullName(true) }}</span>
hx-swap="beforeend"> {% include '_partials/_dot.html.twig' %}
{{ icon('download', 'w-4 h-4 shrink-0') }} <span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
<span>{{ document.owner.teamer.fullName(true) }}</span> {% include '_partials/_dot.html.twig' %}
{% include '_partials/_dot.html.twig' %} <span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
<span>{{ document.typeLabel }}</span> </button>
</button> </li>
</li> {% else %}
{% else %} <li class="py-2 first:pt-0 last:pb-0">
<li class="py-2 first:pt-0 last:pb-0"> -
- </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul> </div>
</div>
<div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2">
<h2 class="flex justify-between pb-2">
<span class="font-bold text-lg">Neue Honorarnoten</span>
{% if newInvoicesCount > 0 or pendingInvoicesCount > 0 %}
<span>{{ newInvoicesCount }} neu, {{ pendingInvoicesCount }} in Bearbeitung</span>
{% endif %}
</h2>
<div class="max-h-64 overflow-y-scroll overflow-x-hidden">
<ul class="divide-y divide-gray-200">
{% for document in newInvoices %}
{% set assignment = document.disposition.assignment %}
<li class="py-2 first:pt-0 last:pb-0">
<button type="button"
class="flex items-center space-x-1 truncate"
hx-get="{{ path('app_administrative_document_check', { 'uuid': document.uuid, 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('download', 'w-4 h-4 shrink-0') }}
<span>{{ document.owner.teamer.fullName(true) }}</span>
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
{% include '_partials/_dot.html.twig' %}
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
</button>
</li>
{% else %}
<li class="py-2 first:pt-0 last:pb-0">
-
</li>
{% endfor %}
</ul>
</div>
</div> </div>
<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">
@@ -53,34 +88,40 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% set overdueFeedbacksCount = overdueFeedbacks|length %}
<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="flex justify-between pb-2">
überfällige Feedbacks <span class="font-bold text-lg">Überfällige Feedbacks</span>
{% if overdueFeedbacksCount > 0 %}
<span>{{ overdueFeedbacksCount }} überfälllig</span>
{% endif %}
</h2> </h2>
<ul class="divide-y divide-gray-200"> <div class="max-h-64 overflow-y-scroll overflow-x-hidden">
{% for disposition in overdueFeedbacks %} <ul class="divide-y divide-gray-200">
{% set assignment = disposition.assignment %} {% for disposition in overdueFeedbacks %}
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden"> {% set assignment = disposition.assignment %}
<a href="{{ path('app_administrative_feedback_provide', { 'uuid': disposition.uuid, 'r': return_url() }) }}" <li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden">
class="flex items-center space-x-1"> <a href="{{ path('app_administrative_feedback_provide', { 'uuid': disposition.uuid, 'r': return_url() }) }}"
{{ icon('feedback', 'w-4 h-4 shrink-0') }} class="flex items-center space-x-1">
<span class="whitespace-nowrap">{{ disposition.teamer }}</span> {{ icon('feedback', 'w-4 h-4 shrink-0') }}
{% if assignment.destination %} <span class="whitespace-nowrap">{{ disposition.teamer }}</span>
{% include '_partials/_dot.html.twig' %} {% if assignment.destination %}
<span class="whitespace-nowrap">{{ assignment.destination }}</span> {% include '_partials/_dot.html.twig' %}
{% endif %} <span class="whitespace-nowrap">{{ assignment.destination }}</span>
{% if assignment.jobProfile %} {% endif %}
{% include '_partials/_dot.html.twig' %} {% if assignment.jobProfile %}
<span class="whitespace-nowrap">{{ assignment.jobProfile.name }}</span> {% include '_partials/_dot.html.twig' %}
{% endif %} <span class="whitespace-nowrap">{{ assignment.jobProfile.name }}</span>
</a> {% endif %}
</li> </a>
{% else %} </li>
<li class="py-2 first:pt-0 last:pb-0"> {% else %}
- <li class="py-2 first:pt-0 last:pb-0">
</li> -
{% endfor %} </li>
</ul> {% endfor %}
</ul>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}