Feat: Implement workflow for disposition document uploads

This commit is contained in:
Björn Fromme
2023-10-13 11:52:32 +02:00
parent 6df725edc4
commit 8dcfa2f43c
7 changed files with 296 additions and 81 deletions
Generated
+21 -21
View File
@@ -101,16 +101,16 @@
}, },
{ {
"name": "doctrine/collections", "name": "doctrine/collections",
"version": "2.1.3", "version": "2.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/collections.git", "url": "https://github.com/doctrine/collections.git",
"reference": "3023e150f90a38843856147b58190aa8b46cc155" "reference": "72328a11443a0de79967104ad36ba7b30bded134"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/collections/zipball/3023e150f90a38843856147b58190aa8b46cc155", "url": "https://api.github.com/repos/doctrine/collections/zipball/72328a11443a0de79967104ad36ba7b30bded134",
"reference": "3023e150f90a38843856147b58190aa8b46cc155", "reference": "72328a11443a0de79967104ad36ba7b30bded134",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -118,7 +118,7 @@
"php": "^8.1" "php": "^8.1"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^10.0", "doctrine/coding-standard": "^12",
"ext-json": "*", "ext-json": "*",
"phpstan/phpstan": "^1.8", "phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-phpunit": "^1.0",
@@ -167,7 +167,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/collections/issues", "issues": "https://github.com/doctrine/collections/issues",
"source": "https://github.com/doctrine/collections/tree/2.1.3" "source": "https://github.com/doctrine/collections/tree/2.1.4"
}, },
"funding": [ "funding": [
{ {
@@ -183,7 +183,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-06T15:15:36+00:00" "time": "2023-10-03T09:22:33+00:00"
}, },
{ {
"name": "doctrine/common", "name": "doctrine/common",
@@ -278,16 +278,16 @@
}, },
{ {
"name": "doctrine/dbal", "name": "doctrine/dbal",
"version": "3.7.0", "version": "3.7.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/dbal.git", "url": "https://github.com/doctrine/dbal.git",
"reference": "00d03067f07482f025d41ab55e4ba0db5eca2cdf" "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/00d03067f07482f025d41ab55e4ba0db5eca2cdf", "url": "https://api.github.com/repos/doctrine/dbal/zipball/5b7bd66c9ff58c04c5474ab85edce442f8081cb2",
"reference": "00d03067f07482f025d41ab55e4ba0db5eca2cdf", "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -371,7 +371,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/dbal/issues", "issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.7.0" "source": "https://github.com/doctrine/dbal/tree/3.7.1"
}, },
"funding": [ "funding": [
{ {
@@ -387,7 +387,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-09-26T20:56:55+00:00" "time": "2023-10-06T05:06:20+00:00"
}, },
{ {
"name": "doctrine/deprecations", "name": "doctrine/deprecations",
@@ -1322,16 +1322,16 @@
}, },
{ {
"name": "egulias/email-validator", "name": "egulias/email-validator",
"version": "4.0.1", "version": "4.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/egulias/EmailValidator.git", "url": "https://github.com/egulias/EmailValidator.git",
"reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff" "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/3a85486b709bc384dae8eb78fb2eec649bdb64ff", "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e",
"reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff", "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1340,8 +1340,8 @@
"symfony/polyfill-intl-idn": "^1.26" "symfony/polyfill-intl-idn": "^1.26"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5.27", "phpunit/phpunit": "^10.2",
"vimeo/psalm": "^4.30" "vimeo/psalm": "^5.12"
}, },
"suggest": { "suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
@@ -1377,7 +1377,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/egulias/EmailValidator/issues", "issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/4.0.1" "source": "https://github.com/egulias/EmailValidator/tree/4.0.2"
}, },
"funding": [ "funding": [
{ {
@@ -1385,7 +1385,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-01-14T14:17:03+00:00" "time": "2023-10-06T06:47:41+00:00"
}, },
{ {
"name": "imagine/imagine", "name": "imagine/imagine",
+3 -3
View File
@@ -1,10 +1,10 @@
framework: framework:
workflows: workflows:
disposition: disposition:
type: state_machine type: 'state_machine'
marking_store: marking_store:
type: method type: 'method'
property: status property: 'status'
supports: supports:
- App\Entity\Disposition - App\Entity\Disposition
initial_marking: new initial_marking: new
@@ -11,6 +11,7 @@ use App\Service\Upload\UploadHandler;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@@ -24,7 +25,8 @@ class DetailController extends AbstractController
public function __construct( public function __construct(
private readonly UploadHandler $uploadHandler, private readonly UploadHandler $uploadHandler,
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
private readonly WorkflowInterface $dispositionStateMachine, #[Target('disposition')]
private readonly WorkflowInterface $workflow,
private readonly LoggerInterface $logger private readonly LoggerInterface $logger
) { ) {
} }
@@ -41,10 +43,10 @@ class DetailController extends AbstractController
$uploadSession = $this->uploadHandler->getUploadSession(); $uploadSession = $this->uploadHandler->getUploadSession();
if (0 < $uploadSession->getCount()) { if (0 < $uploadSession->getCount()) {
if (true === $this->dispositionStateMachine->can($disposition, 'upload_contract')) { if (true === $this->workflow->can($disposition, 'upload_contract')) {
$this->uploadContract($disposition, $uploadSession); $this->uploadContract($disposition, $uploadSession);
} }
if (true === $this->dispositionStateMachine->can($disposition, 'upload_invoice')) { if (true === $this->workflow->can($disposition, 'upload_invoice')) {
$this->uploadInvoice($disposition, $uploadSession); $this->uploadInvoice($disposition, $uploadSession);
} }
} }
@@ -74,7 +76,7 @@ class DetailController extends AbstractController
$upload->setStatus(Upload::STATUS_NEW); $upload->setStatus(Upload::STATUS_NEW);
$disposition->addDocument($upload); $disposition->addDocument($upload);
$this->dispositionStateMachine->apply($disposition, 'upload_contract'); $this->workflow->apply($disposition, 'upload_contract');
$this->entityManager->flush(); $this->entityManager->flush();
@@ -101,7 +103,7 @@ class DetailController extends AbstractController
$upload->setStatus(Upload::STATUS_NEW); $upload->setStatus(Upload::STATUS_NEW);
$disposition->addDocument($upload); $disposition->addDocument($upload);
$this->dispositionStateMachine->apply($disposition, 'upload_invoice'); $this->workflow->apply($disposition, 'upload_invoice');
$this->entityManager->flush(); $this->entityManager->flush();
@@ -0,0 +1,78 @@
<?php
namespace App\EventListener;
use App\Entity\Disposition;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Workflow\Event\GuardEvent;
class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly TranslatorInterface $translator)
{}
public static function getSubscribedEvents(): array
{
return [
'workflow.disposition.guard.upload_contract' => ['guardUploadContract'],
'workflow.disposition.guard.upload_invoice' => ['guardUploadInvoice'],
];
}
public function guardUploadContract(GuardEvent $event): void
{
/** @var Disposition $disposition */
$disposition = $event->getSubject();
$assignment = $disposition->getAssignment();
$assignmentPeriod = $assignment->getTotalPeriod();
$today = new \DateTimeImmutable();
$dueDateFrom = $disposition->getCreatedAt()->modify('+ 7days');
$dueDateTo = $assignmentPeriod->end->modify('-1 day');
if ($dueDateTo < $today) {
$message = $this
->translator
->trans('message.upload_contract.too_late', [
'%date_from%' => $dueDateFrom->format('d.m.Y'),
'%date_to%' => $dueDateTo->format('d.m.Y'),
])
;
$event->setBlocked(true, $message);
}
}
public function guardUploadInvoice(GuardEvent $event): void
{
/** @var Disposition $disposition */
$disposition = $event->getSubject();
$assignment = $disposition->getAssignment();
$destination = $assignment->getDestination();
$today = new \DateTimeImmutable();
$earliestDate = $destination->getDateFrom();
if ($earliestDate > $today) {
$message = $this
->translator
->trans('message.upload_invoice.too_early', [
'%date%' => $earliestDate->format('d.m.Y'),
])
;
$event->setBlocked(true, $message);
}
$dueDate = $destination->getDateTo()->modify('+14 days');
if ($dueDate < $today) {
$message = $this
->translator
->trans('message.upload_invoice.too_late', [
'%date%' => $dueDate->format('d.m.Y'),
])
;
$event->setBlocked(true, $message);
}
}
}
+84
View File
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (20230911.1827)
-->
<!-- Title: workflow Pages: 1 -->
<svg width="813pt" height="188pt"
viewBox="0.00 0.00 812.67 188.14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184.14)">
<title>workflow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-184.14 808.67,-184.14 808.67,4 -4,4"/>
<!-- place_c2a6b03f190dfb2b4aa91f8af8d477a9bc3401dc -->
<g id="node1" class="node">
<title>place_c2a6b03f190dfb2b4aa91f8af8d477a9bc3401dc</title>
<ellipse fill="lightblue" stroke="#333333" cx="36" cy="-53" rx="36" ry="36"/>
<text text-anchor="middle" x="36" y="-49.7" font-family="Arial" font-size="9.00">new</text>
</g>
<!-- place_1c0139083383c818fd7ddf51e98b729b6b19803d -->
<g id="node2" class="node">
<title>place_1c0139083383c818fd7ddf51e98b729b6b19803d</title>
<ellipse fill="none" stroke="#333333" cx="220.69" cy="-53" rx="46.69" ry="46.69"/>
<text text-anchor="middle" x="220.69" y="-49.7" font-family="Arial" font-size="9.00">checking_contract</text>
</g>
<!-- place_c2a6b03f190dfb2b4aa91f8af8d477a9bc3401dc&#45;&gt;place_1c0139083383c818fd7ddf51e98b729b6b19803d -->
<g id="edge1" class="edge">
<title>place_c2a6b03f190dfb2b4aa91f8af8d477a9bc3401dc&#45;&gt;place_1c0139083383c818fd7ddf51e98b729b6b19803d</title>
<path fill="none" stroke="#333333" d="M72.22,-53C99.14,-53 136.66,-53 167.12,-53"/>
<polygon fill="#333333" stroke="#333333" points="166.99,-54.75 171.99,-53 166.99,-51.25 166.99,-54.75"/>
<text text-anchor="middle" x="123" y="-54.95" font-family="Arial" font-size="9.00">upload_contract</text>
</g>
<!-- place_d35ce8acb0d0ca09297349a4e2d4240c2819c523 -->
<g id="node3" class="node">
<title>place_d35ce8acb0d0ca09297349a4e2d4240c2819c523</title>
<ellipse fill="none" stroke="#333333" cx="406.89" cy="-53" rx="36" ry="36"/>
<text text-anchor="middle" x="406.89" y="-49.7" font-family="Arial" font-size="9.00">confirmed</text>
</g>
<!-- place_1c0139083383c818fd7ddf51e98b729b6b19803d&#45;&gt;place_d35ce8acb0d0ca09297349a4e2d4240c2819c523 -->
<g id="edge2" class="edge">
<title>place_1c0139083383c818fd7ddf51e98b729b6b19803d&#45;&gt;place_d35ce8acb0d0ca09297349a4e2d4240c2819c523</title>
<path fill="none" stroke="#333333" d="M267.7,-53C297.32,-53 335.51,-53 364.23,-53"/>
<polygon fill="#333333" stroke="#333333" points="364.06,-54.75 369.06,-53 364.06,-51.25 364.06,-54.75"/>
<text text-anchor="middle" x="319.14" y="-54.95" font-family="Arial" font-size="9.00">confirm_contract</text>
</g>
<!-- place_7b74ca224881f644aaf11239b7a05c62a7834047 -->
<g id="node4" class="node">
<title>place_7b74ca224881f644aaf11239b7a05c62a7834047</title>
<ellipse fill="none" stroke="#333333" cx="587.03" cy="-135" rx="45.14" ry="45.14"/>
<text text-anchor="middle" x="587.03" y="-131.7" font-family="Arial" font-size="9.00">checking_invoice</text>
</g>
<!-- place_d35ce8acb0d0ca09297349a4e2d4240c2819c523&#45;&gt;place_7b74ca224881f644aaf11239b7a05c62a7834047 -->
<g id="edge3" class="edge">
<title>place_d35ce8acb0d0ca09297349a4e2d4240c2819c523&#45;&gt;place_7b74ca224881f644aaf11239b7a05c62a7834047</title>
<path fill="none" stroke="#333333" d="M440.15,-67.85C467.99,-80.67 508.55,-99.34 539.83,-113.74"/>
<polygon fill="#333333" stroke="#333333" points="538.92,-115.24 544.19,-115.74 540.38,-112.06 538.92,-115.24"/>
<text text-anchor="middle" x="492.39" y="-108.95" font-family="Arial" font-size="9.00">upload_invoice</text>
</g>
<!-- place_231e564db4cdb44a6545583a8d460edc7f9f97ca -->
<g id="node6" class="node">
<title>place_231e564db4cdb44a6545583a8d460edc7f9f97ca</title>
<ellipse fill="none" stroke="#333333" cx="587.03" cy="-36" rx="36" ry="36"/>
<text text-anchor="middle" x="587.03" y="-32.7" font-family="Arial" font-size="9.00">completed</text>
</g>
<!-- place_d35ce8acb0d0ca09297349a4e2d4240c2819c523&#45;&gt;place_231e564db4cdb44a6545583a8d460edc7f9f97ca -->
<g id="edge4" class="edge">
<title>place_d35ce8acb0d0ca09297349a4e2d4240c2819c523&#45;&gt;place_231e564db4cdb44a6545583a8d460edc7f9f97ca</title>
<path fill="none" stroke="#333333" d="M443.08,-49.64C472.24,-46.86 513.73,-42.9 544.51,-39.96"/>
<polygon fill="#333333" stroke="#333333" points="544.59,-41.71 549.4,-39.5 544.25,-38.23 544.59,-41.71"/>
<text text-anchor="middle" x="492.39" y="-49.95" font-family="Arial" font-size="9.00">complete</text>
</g>
<!-- place_9e1f1120d2eedc498808e1d855cfdbbd5564f22b -->
<g id="node5" class="node">
<title>place_9e1f1120d2eedc498808e1d855cfdbbd5564f22b</title>
<ellipse fill="none" stroke="#333333" cx="768.67" cy="-135" rx="36" ry="36"/>
<text text-anchor="middle" x="768.67" y="-131.7" font-family="Arial" font-size="9.00">paid</text>
</g>
<!-- place_7b74ca224881f644aaf11239b7a05c62a7834047&#45;&gt;place_9e1f1120d2eedc498808e1d855cfdbbd5564f22b -->
<g id="edge5" class="edge">
<title>place_7b74ca224881f644aaf11239b7a05c62a7834047&#45;&gt;place_9e1f1120d2eedc498808e1d855cfdbbd5564f22b</title>
<path fill="none" stroke="#333333" d="M632.43,-135C661.02,-135 697.95,-135 725.99,-135"/>
<polygon fill="#333333" stroke="#333333" points="725.68,-136.75 730.68,-135 725.68,-133.25 725.68,-136.75"/>
<text text-anchor="middle" x="682.42" y="-136.95" font-family="Arial" font-size="9.00">confirm_invoice</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

+48 -5
View File
@@ -2,6 +2,13 @@
{% block title %}Einsatzdetails{% endblock %} {% block title %}Einsatzdetails{% endblock %}
{% macro infobox(message) %}
<div class="flex items-start space-x-4 bg-primary text-white p-4 rounded-md">
{{ icon('info', 'w-6 h-6 shrink-0') }}
<span>{{ message }}</span>
</div>
{% endmacro %}
{% block content %} {% block content %}
<h1 class="text-2xl font-bold pb-8"> <h1 class="text-2xl font-bold pb-8">
Einsatzdetails Einsatzdetails
@@ -20,6 +27,9 @@
<br> <br>
{{ assignment.contact }}, <a href="mailto:{{ assignment.contact.email }}" class="underline">{{ assignment.contact.email }}</a> {{ assignment.contact }}, <a href="mailto:{{ assignment.contact.email }}" class="underline">{{ assignment.contact.email }}</a>
</p> </p>
{# Upload contract #}
{% if workflow_has_marked_place(disposition, 'new') %}
{% if workflow_can(disposition, 'upload_contract') %} {% if workflow_can(disposition, 'upload_contract') %}
<h3 class="text-xl font-bold pb-4"> <h3 class="text-xl font-bold pb-4">
Hier findest du deinen Honorarvertrag Hier findest du deinen Honorarvertrag
@@ -44,7 +54,16 @@
</button> </button>
{{ form_rest(form) }} {{ form_rest(form) }}
{{ form_end(form) }} {{ form_end(form) }}
{% elseif workflow_can(disposition, 'upload_invoice') %} {% else %}
{% for blocker in workflow_transition_blockers(disposition, 'upload_contract') %}
{{ _self.infobox(blocker.message) }}
{% endfor %}
{% endif %}
{% endif %}
{# Upload invoice #}
{% if workflow_has_marked_place(disposition, 'confirmed') %}
{% if workflow_can(disposition, 'upload_invoice') %}
<h3 class="text-xl font-bold pb-4"> <h3 class="text-xl font-bold pb-4">
Hier findest du deine Honorarnote Hier findest du deine Honorarnote
</h3> </h3>
@@ -68,23 +87,47 @@
</button> </button>
{{ form_rest(form) }} {{ form_rest(form) }}
{{ form_end(form) }} {{ form_end(form) }}
{% elseif workflow_has_marked_place(disposition, 'checking_contract') %} {% else %}
{% for blocker in workflow_transition_blockers(disposition, 'upload_invoice') %}
{{ _self.infobox(blocker.message) }}
{% endfor %}
{% endif %}
{% endif %}
{# Contract uploaded #}
{% if workflow_has_marked_place(disposition, 'checking_contract') %}
<h3 class="text-xl font-bold pb-4"> <h3 class="text-xl font-bold pb-4">
Danke für den Upload des Honorarvertrags Danke für den Upload des Honorarvertrags
</h3> </h3>
{% elseif workflow_has_marked_place(disposition, 'checking_invoice') %} <div class="pb-4">
Wir prüfen das Dokument schnellstmöglich.
</div>
{% endif %}
{# Invoice uploaded #}
{% if workflow_has_marked_place(disposition, 'checking_invoice') %}
<h3 class="text-xl font-bold pb-4"> <h3 class="text-xl font-bold pb-4">
Danke für den Upload der Honorarnote Danke für den Upload der Honorarnote
</h3> </h3>
{% elseif workflow_has_marked_place(disposition, 'paid') %} <div class="pb-4">
Wir prüfen das Dokument schnellstmöglich.
</div>
{% endif %}
{# Invoice paid #}
{% if workflow_has_marked_place(disposition, 'paid') %}
<h3 class="text-xl font-bold pb-4"> <h3 class="text-xl font-bold pb-4">
Dein Honorar wurde überwiesen Dein Honorar wurde überwiesen
</h3> </h3>
{% else %} {% endif %}
{# No invoice required #}
{% if workflow_has_marked_place(disposition, 'completed') %}
<h3 class="text-xl font-bold pb-4"> <h3 class="text-xl font-bold pb-4">
Dein Einsatz ist abgeschlossen Dein Einsatz ist abgeschlossen
</h3> </h3>
{% endif %} {% endif %}
<div class="pt-4"> <div class="pt-4">
<a href="{{ returnUrl }}" class="underline"> <a href="{{ returnUrl }}" class="underline">
zurück zurück
+8
View File
@@ -22,12 +22,20 @@ label:
payed: bezahlt payed: bezahlt
error: Fehler error: Fehler
message:
upload_contract:
too_late: 'Die Frist für den Upload des Honorarvertrages ist abgelaufen (%date_from% - %date_to%). Bitte wende dich an deinen Ansprechpartner'
upload_invoice:
too_early: 'Du kannst deine Honorarnote mit Beginn deines Einsatzes ab dem %date% einreichen.'
too_late: 'Du hättest deine Honorarnote bis zum %date% einreichen müssen. Bitte wende dich an deinen Ansprechpartner.'
uploader: uploader:
drop_files: Dateien hier ablegen oder drop_files: Dateien hier ablegen oder
click_here: hier klicken click_here: hier klicken
file_too_big: Die maximale Dateigröße beträgt %maxFilesize%MB file_too_big: Die maximale Dateigröße beträgt %maxFilesize%MB
invalid_file_type: Der Dateityp ist nicht erlaubt invalid_file_type: Der Dateityp ist nicht erlaubt
max_files_exceeded: Die maximale Anzahl von Dateien is %maxFilesExceeded% max_files_exceeded: Die maximale Anzahl von Dateien is %maxFilesExceeded%
paginator: paginator:
previous: vorherige previous: vorherige
next: nächste next: nächste