feat: dedicated list view for document batch downloading

This commit is contained in:
Björn Fromme
2025-02-01 11:42:21 +01:00
parent 3f839f9082
commit 24c8c46ca4
7 changed files with 195 additions and 18 deletions
@@ -8,7 +8,6 @@ use App\Service\Upload\UploadHandler;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -32,9 +31,9 @@ class BatchDownloadController extends AbstractController
defaults: ['type' => Upload::TYPE_INVOICE] defaults: ['type' => Upload::TYPE_INVOICE]
)] )]
#[IsGranted('ROLE_ADMINISTRATIVE')] #[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(string $type, Request $request): Response public function index(string $type): Response
{ {
$status = $request->query->get('status', Upload::STATUS_PAID); $status = Upload::TYPE_INVOICE === $type ? Upload::STATUS_PAID : Upload::STATUS_CHECKED;
$uploads = $this $uploads = $this
->uploadRepository ->uploadRepository
@@ -47,7 +46,7 @@ class BatchDownloadController extends AbstractController
return $this->redirectToRoute('app_administrative_document_index'); return $this->redirectToRoute('app_administrative_document_index');
} }
$zipFilename = sprintf('Honorarnoten_%s.zip', date('YmdHi')); $zipFilename = sprintf('%ss_%s.zip', $type, date('YmdHi'));
$response = new StreamedResponse(function () use ($uploads, $zipFilename) { $response = new StreamedResponse(function () use ($uploads, $zipFilename) {
$zip = new ZipStream( $zip = new ZipStream(
@@ -2,6 +2,8 @@
namespace App\Controller\Administrative\Document; namespace App\Controller\Administrative\Document;
use App\Entity\Upload;
use App\Model\DocumentFilterDto;
use App\Repository\UploadRepository; use App\Repository\UploadRepository;
use App\Service\Common\DocumentFilterHandler; use App\Service\Common\DocumentFilterHandler;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
@@ -42,4 +44,32 @@ class IndexController extends AbstractController
'filterDto' => $filterDto, 'filterDto' => $filterDto,
]); ]);
} }
#[Route(
path: '/administrative/document/accepted/{type}',
name: 'app_administrative_document_accepted',
defaults: ['type' => Upload::TYPE_INVOICE]
)]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function accepted(string $type, Request $request): Response
{
$status = Upload::TYPE_INVOICE === $type ? Upload::STATUS_PAID : Upload::STATUS_CHECKED;
$query = $this
->uploadRepository
->getBatchDownloadListQuery($type, $status)
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
$request->query->getInt('limit', 50),
);
return $this->render('administrative/document/accepted.html.twig', [
'pagination' => $pagination,
'type' => $type,
'status' => $status,
]);
}
} }
+8 -6
View File
@@ -33,12 +33,14 @@ class DownloadController extends AbstractController
$inline = (bool) $request->get('inline'); $inline = (bool) $request->get('inline');
$disposition = $inline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT; $disposition = $inline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT;
// mark file downloaded // mark file downloaded if applicable
$upload if ($this->isGranted('ROLE_ADMINISTRATIVE' && false === $inline)) {
->setDownloaded() $upload
->setDownloadedBy($this->getUser()->getUserIdentifier()) ->setDownloaded()
; ->setDownloadedBy($this->getUser()->getUserIdentifier())
$this->entityManager->flush(); ;
$this->entityManager->flush();
}
$this->logger->info('Download file', [ $this->logger->info('Download file', [
'file_id' => $upload->getId(), 'file_id' => $upload->getId(),
+20 -1
View File
@@ -2,6 +2,7 @@
namespace App\Menu; namespace App\Menu;
use App\Entity\Upload;
use Knp\Menu\ItemInterface; use Knp\Menu\ItemInterface;
class AdminMenuBuilder extends AbstractMenuBuilder class AdminMenuBuilder extends AbstractMenuBuilder
@@ -40,7 +41,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'icon' => 'mail', 'icon' => 'mail',
], ],
]); ]);
$menu->addChild('Dokumentenübersicht', [ $documentMenu = $menu->addChild('Dokumentenübersicht', [
'route' => 'app_administrative_document_index', 'route' => 'app_administrative_document_index',
'linkAttributes' => [ 'linkAttributes' => [
'title' => 'Dokumentenübersicht', 'title' => 'Dokumentenübersicht',
@@ -49,6 +50,24 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'icon' => 'document', 'icon' => 'document',
], ],
]); ]);
$documentMenu->addChild('akzeptierte Honorarnoten', [
'route' => 'app_administrative_document_accepted',
'routeParameters' => [
'type' => Upload::TYPE_INVOICE,
],
'linkAttributes' => [
'title' => 'akzeptierte Honorarnoten',
],
]);
$documentMenu->addChild('geprüfte Honorverträge', [
'route' => 'app_administrative_document_accepted',
'routeParameters' => [
'type' => Upload::TYPE_CONTRACT,
],
'linkAttributes' => [
'title' => 'geprüfte Honorverträge',
],
]);
$menu->addChild('Feedbackübersicht', [ $menu->addChild('Feedbackübersicht', [
'route' => 'app_administrative_feedback_index', 'route' => 'app_administrative_feedback_index',
'linkAttributes' => [ 'linkAttributes' => [
+21 -3
View File
@@ -80,12 +80,21 @@ class UploadRepository extends ServiceEntityRepository
return $data; return $data;
} }
public function getBatchDownloadList( public function getBatchDownloadListQuery(
string $type = Upload::TYPE_INVOICE, string $type = Upload::TYPE_INVOICE,
string $status = Upload::STATUS_PAID string $status = Upload::STATUS_PAID
): array { ): Query {
$qb = $this->createQueryBuilder('upload'); $qb = $this->createQueryBuilder('upload');
$qb
->select('upload', 'disposition', 'assignment', 'job_profile', 'destination', 'teamer')
->innerJoin('upload.disposition', 'disposition')
->innerJoin('disposition.assignment', 'assignment')
->innerJoin('assignment.jobProfile', 'job_profile')
->innerJoin('assignment.destination', 'destination')
->innerJoin('disposition.teamer', 'teamer')
;
return $qb->where( return $qb->where(
$qb->expr()->andX( $qb->expr()->andX(
$qb->expr()->eq('upload.type', ':type'), $qb->expr()->eq('upload.type', ':type'),
@@ -95,10 +104,19 @@ class UploadRepository extends ServiceEntityRepository
->setParameter('type', $type) ->setParameter('type', $type)
->setParameter('status', $status) ->setParameter('status', $status)
->getQuery() ->getQuery()
->getResult()
; ;
} }
public function getBatchDownloadList(
string $type = Upload::TYPE_INVOICE,
string $status = Upload::STATUS_PAID
): array {
return $this
->getBatchDownloadListQuery($type, $status)
->getResult()
;
}
public function getUploadListQuery(DocumentFilterDto $filterDto): Query public function getUploadListQuery(DocumentFilterDto $filterDto): Query
{ {
$qb = $this->createQueryBuilder('upload'); $qb = $this->createQueryBuilder('upload');
@@ -0,0 +1,113 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Dokumentenübersicht{% endblock %}
{% block content %}
<div class="flex items-start justify-between pb-4">
<h1 class="text-2xl font-bold">
{% if type == constant('App\\Entity\\Upload::TYPE_INVOICE') %}
Übersicht akzeptierte Honorarnoten
{% elseif type == constant('App\\Entity\\Upload::TYPE_CONTRACT') %}
Übersicht geprüfte Honorarverträge
{% else %}
Übersicht
{% endif %}
</h1>
<div class="flex flex-col items-end space-y-2 md:flex-row md:items-center md:space-x-2 md:space-y-0">
<a href="{{ path('app_administrative_document_batch_download', { 'type': type, 'status': status }) }}"
class="btn btn--small">
{{ icon('download', 'w-4 h-4 shrink-0') }}
<span>alle herunterladen</span>
</a>
</div>
</div>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Einsatz&shy;zeitraum', 'destination.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Destination', 'destination.product') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Jobprofil', 'job_profile.name') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Dokument', 'upload.type') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Teamer:in', 'teamer.lastName') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Status', 'upload.status') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for upload in pagination %}
{% set disposition = upload.disposition %}
{% set assignment = disposition.assignment %}
<tr>
<td>
{{ assignment.destination.dateFrom|date('d.m.Y') }}
-
{{ assignment.destination.dateTo|date('d.m.Y') }}
</td>
<td>
<div class="flex items-start space-x-2">
{{ icon('flag-' ~ assignment.destination.country, 'w-5 h-5 shrink-0') }}
<div class="flex-1">
{{ assignment.destination.product }}
<br>
{{ assignment.destination.hotel }}
</div>
</div>
</td>
<td>
{{ assignment.jobProfile.name }}
</td>
<td>
{{ ('label.document.type.' ~ upload.type)|trans }}
</td>
<td>
{{ disposition.teamer }}
</td>
<td>
{{ upload.status|upload_status_badge }}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
{% if is_granted('DELETE', upload) %}
<button type="button"
class="text-red-500"
hx-get="{{ path('app_administrative_document_delete', { 'uuid': upload.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('delete') }}
</button>
{% endif %}
<a href="{{ path('app_common_download', { 'uuid': upload.uuid, 'inline': false }) }}"
target="_blank"
title="Download">
{{ icon('download') }}
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7">
Keine Daten...
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endblock %}
@@ -8,10 +8,6 @@
Dokumentenübersicht Dokumentenübersicht
</h1> </h1>
<div class="flex flex-col items-end space-y-2 md:flex-row md:items-center md:space-x-2 md:space-y-0"> <div class="flex flex-col items-end space-y-2 md:flex-row md:items-center md:space-x-2 md:space-y-0">
<a href="{{ path('app_administrative_document_batch_download') }}" class="btn btn--small btn--secondary">
{{ icon('download', 'w-4 h-4 shrink-0') }}
<span>best. Honorarnoten</span>
</a>
<button type="button" <button type="button"
class="{{ html_classes('btn btn--small', { 'btn--secondary': filterDto.active }) }}" class="{{ html_classes('btn btn--small', { 'btn--secondary': filterDto.active }) }}"
title="Filter" title="Filter"