feat: call off single disposition as admin with optional notification
This commit is contained in:
@@ -29,3 +29,8 @@ yarn-error.log
|
||||
###> liip/imagine-bundle ###
|
||||
/public/media/cache/
|
||||
###< liip/imagine-bundle ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
+3
-1
@@ -113,7 +113,8 @@
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
],
|
||||
"php-cs-fixer": "php-cs-fixer --rules=@Symfony"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
@@ -126,6 +127,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"deployer/deployer": "^7.3",
|
||||
"friendsofphp/php-cs-fixer": "^3.84",
|
||||
"marcocesarato/php-conventional-changelog": "^1.17",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
|
||||
Generated
+1030
-1
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ framework:
|
||||
- ended
|
||||
- checking_invoice
|
||||
- completed
|
||||
- called_off
|
||||
transitions:
|
||||
upload_contract:
|
||||
from: new
|
||||
|
||||
@@ -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 Version20250722125647 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 disposition ADD called_off_by VARCHAR(64) DEFAULT NULL, ADD called_off_reason LONGTEXT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE disposition DROP called_off_by, DROP called_off_reason');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\Disposition;
|
||||
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
use App\Event\DispositionCalledOffEvent;
|
||||
use App\Form\DispositionCallOffType;
|
||||
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;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class CallOffController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/disposition/call-off/{uuid}', name: 'app_administrative_disposition_call_off')]
|
||||
#[IsGranted('CALL_OFF', subject: 'disposition')]
|
||||
public function index(Disposition $disposition, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(DispositionCallOffType::class, $disposition, ['hx_post' => $request->getUri()]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$disposition->setStatus(Assignment::STATUS_CALLED_OFF);
|
||||
$sendNotification = $form->get('sendNotification')->getData();
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this
|
||||
->eventDispatcher
|
||||
->dispatch(new DispositionCalledOffEvent($disposition, $sendNotification), DispositionCalledOffEvent::NAME)
|
||||
;
|
||||
|
||||
$assignment = $disposition->getAssignment();
|
||||
|
||||
$this->logger->info('Call off disposition', [
|
||||
'assignment_id' => $disposition->getId(),
|
||||
'destination' => (string) $assignment->getDestination(),
|
||||
'job_profile' => $assignment->getJobProfile()->getName(),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Der Einsatz wurde abgesagt');
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
|
||||
'uuid' => $assignment->getUuid(),
|
||||
]));
|
||||
}
|
||||
|
||||
return $this->render('administrative/disposition/modal_call_off.html.twig', [
|
||||
'disposition' => $disposition,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -487,6 +487,13 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
});
|
||||
}
|
||||
|
||||
public function getActiveDispositions(): Collection
|
||||
{
|
||||
return $this->dispositions->filter(function (Disposition $disposition) {
|
||||
return Disposition::STATUS_CALLED_OFF !== $disposition->getStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public function getConfirmedDispositions(): Collection
|
||||
{
|
||||
return $this->dispositions->filter(function (Disposition $disposition) {
|
||||
|
||||
@@ -25,6 +25,10 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
public const STATUS_CHECKING_INVOICE = 'checking_invoice';
|
||||
public const STATUS_PAID = 'paid';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_CALLED_OFF = 'called_off';
|
||||
|
||||
public const CALLED_OFF_BY_TEAMER = 'teamer';
|
||||
public const CALLED_OFF_BY_OFFICE = 'office';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -59,6 +63,12 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
#[ORM\JoinColumn(onDelete: 'SET NULL')]
|
||||
private ?Feedback $feedback = null;
|
||||
|
||||
#[ORM\Column(length: 64, nullable: true)]
|
||||
private ?string $calledOffBy = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $calledOffReason = null;
|
||||
|
||||
public function __construct(Application $application)
|
||||
{
|
||||
$this->uuid = Uuid::v4();
|
||||
@@ -243,4 +253,28 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
|
||||
return $period->isStarted();
|
||||
}
|
||||
|
||||
public function getCalledOffBy(): ?string
|
||||
{
|
||||
return $this->calledOffBy;
|
||||
}
|
||||
|
||||
public function setCalledOffBy(?string $calledOffBy): static
|
||||
{
|
||||
$this->calledOffBy = $calledOffBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCalledOffReason(): ?string
|
||||
{
|
||||
return $this->calledOffReason;
|
||||
}
|
||||
|
||||
public function setCalledOffReason(?string $calledOffReason): static
|
||||
{
|
||||
$this->calledOffReason = $calledOffReason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Disposition;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class DispositionCalledOffEvent extends Event
|
||||
{
|
||||
public const NAME = 'disposition.called_off';
|
||||
|
||||
public function __construct(private readonly Disposition $disposition, private readonly bool $sendNotification = false)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDisposition(): Disposition
|
||||
{
|
||||
return $this->disposition;
|
||||
}
|
||||
|
||||
public function isSendNotification(): bool
|
||||
{
|
||||
return $this->sendNotification;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Entity\Application;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\ApplicationStatusEvent;
|
||||
use App\Event\AssignmentCalledOffEvent;
|
||||
use App\Event\DispositionCalledOffEvent;
|
||||
use App\Event\DispositionCreatedEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\Event\DocumentRejectedEvent;
|
||||
@@ -21,7 +22,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
public function __construct(
|
||||
private readonly Mailer $mailer,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly ContractRenderer $contractRenderer
|
||||
private readonly ContractRenderer $contractRenderer,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
return [
|
||||
DispositionCreatedEvent::NAME => 'onDispositionCreated',
|
||||
DispositionCalledOffEvent::NAME => 'onDispositionCalledOff',
|
||||
DocumentUploadedEvent::NAME => 'onDocumentUploaded',
|
||||
DocumentRejectedEvent::NAME => 'onDocumentRejected',
|
||||
ApplicationStatusEvent::NAME => 'onApplicationStatus',
|
||||
@@ -38,7 +40,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about accepted application
|
||||
* Notify teamer about accepted application.
|
||||
*/
|
||||
public function onDispositionCreated(DispositionCreatedEvent $event): void
|
||||
{
|
||||
@@ -97,7 +99,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify managers about uploaded documents
|
||||
* Notify managers about uploaded documents.
|
||||
*/
|
||||
public function onDocumentUploaded(DocumentUploadedEvent $event): void
|
||||
{
|
||||
@@ -142,7 +144,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about rejected document
|
||||
* Notify teamer about rejected document.
|
||||
*/
|
||||
public function onDocumentRejected(DocumentRejectedEvent $event): void
|
||||
{
|
||||
@@ -168,7 +170,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about rejected application
|
||||
* Notify teamer about rejected application.
|
||||
*/
|
||||
public function onApplicationStatus(ApplicationStatusEvent $event): void
|
||||
{
|
||||
@@ -189,6 +191,31 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about called-off disposition (if applicable).
|
||||
*/
|
||||
public function onDispositionCalledOff(DispositionCalledOffEvent $event): void
|
||||
{
|
||||
if (false === $event->isSendNotification()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$disposition = $event->getDisposition();
|
||||
$teamer = $disposition->getTeamer();
|
||||
|
||||
$this->mailer->createAndSendEmail([
|
||||
'assignment' => $disposition->getAssignment(),
|
||||
'reason' => $disposition->getCalledOffReason(),
|
||||
], [
|
||||
'to' => $teamer->getCommunication()->getEmail(),
|
||||
'subject' => 'Dein Einsatz wurde abgesagt',
|
||||
'template' => 'email/disposition_called_off.html.twig',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all assigned teamers about called-off assignment.
|
||||
*/
|
||||
public function onAssignmentCalledOff(AssignmentCalledOffEvent $event): void
|
||||
{
|
||||
$assignment = $event->getAssignment();
|
||||
@@ -204,4 +231,4 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Entity\Upload;
|
||||
use App\Event\ApplicationCreatedEvent;
|
||||
use App\Event\ApplicationDeletedEvent;
|
||||
use App\Event\ApplicationStatusEvent;
|
||||
use App\Event\DispositionCalledOffEvent;
|
||||
use App\Event\DispositionCreatedEvent;
|
||||
use App\Event\DispositionDeletedEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
@@ -27,6 +28,7 @@ class StaffingStatusSubscriber implements EventSubscriberInterface
|
||||
ApplicationStatusEvent::NAME => 'onApplicationStatusUpdated',
|
||||
DispositionCreatedEvent::NAME => 'onDispositionCreated',
|
||||
DispositionDeletedEvent::NAME => 'onDispositionDeleted',
|
||||
DispositionCalledOffEvent::NAME => 'onDispositionCalledOff',
|
||||
DocumentConfirmedEvent::NAME => 'onDocumentConfirmed',
|
||||
];
|
||||
}
|
||||
@@ -67,6 +69,13 @@ class StaffingStatusSubscriber implements EventSubscriberInterface
|
||||
$this->updateStaffingStatus($assignment);
|
||||
}
|
||||
|
||||
public function onDispositionCalledOff(DispositionCalledOffEvent $event): void
|
||||
{
|
||||
$assignment = $event->getDisposition()->getAssignment();
|
||||
|
||||
$this->updateStaffingStatus($assignment);
|
||||
}
|
||||
|
||||
public function onDocumentConfirmed(DocumentConfirmedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
@@ -87,4 +96,4 @@ class StaffingStatusSubscriber implements EventSubscriberInterface
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Disposition;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
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;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class DispositionCallOffType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('calledOffBy', ChoiceType::class, [
|
||||
'label' => 'abgesagt durch',
|
||||
'choices' => [
|
||||
'Teamer:in' => Disposition::CALLED_OFF_BY_TEAMER,
|
||||
'E&P' => Disposition::CALLED_OFF_BY_OFFICE,
|
||||
],
|
||||
])
|
||||
->add('calledOffReason', TextareaType::class, [
|
||||
'label' => 'Begründung',
|
||||
'attr' => [
|
||||
'data-controller' => 'textarea-autosize',
|
||||
'data-action' => 'textarea-autosize#resize',
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Bitte gib die Begründung ein',
|
||||
]),
|
||||
],
|
||||
])
|
||||
->add('sendNotification', CheckboxType::class, [
|
||||
'label' => 'Benachrichtigung mit Begründung versenden',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Disposition::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class DispositionVoter extends Voter
|
||||
public const CONTRACT_SUPPLEMENTARY = 'CONTRACT_SUPPLEMENTARY';
|
||||
public const INVOICE = 'INVOICE';
|
||||
public const FEEDBACK = 'FEEDBACK';
|
||||
public const CALL_OFF = 'CALL_OFF';
|
||||
|
||||
public function __construct(private readonly Security $security)
|
||||
{
|
||||
@@ -37,6 +38,7 @@ class DispositionVoter extends Voter
|
||||
static::CONTRACT_SUPPLEMENTARY,
|
||||
static::INVOICE,
|
||||
static::FEEDBACK,
|
||||
static::CALL_OFF,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -53,6 +55,8 @@ class DispositionVoter extends Voter
|
||||
static::FEEDBACK => $this->security->isGranted('ROLE_ADMINISTRATIVE')
|
||||
|| $this->assertHouseManagerAccess($token, $disposition),
|
||||
static::CONTRACT_SUPPLEMENTARY => $this->assertContractUploadAllowed($disposition),
|
||||
static::CALL_OFF => $this->security->isGranted('ROLE_ADMINISTRATIVE')
|
||||
&& Disposition::STATUS_CALLED_OFF !== $disposition->getStatus(),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
@@ -63,6 +67,10 @@ class DispositionVoter extends Voter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Disposition::STATUS_CALLED_OFF === $disposition->getStatus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $token->getUser();
|
||||
|
||||
@@ -95,6 +103,10 @@ class DispositionVoter extends Voter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Disposition::STATUS_CALLED_OFF === $disposition->getStatus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null === $disposition->getDocumentByType(Upload::TYPE_CONTRACT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,18 @@
|
||||
"config/packages/flagception.yaml"
|
||||
]
|
||||
},
|
||||
"friendsofphp/php-cs-fixer": {
|
||||
"version": "3.84",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||
},
|
||||
"files": [
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"knplabs/knp-menu-bundle": {
|
||||
"version": "v3.2.0"
|
||||
},
|
||||
|
||||
@@ -170,7 +170,11 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ disposition.remarks|default('-')|nl2br }}
|
||||
{% if disposition.calledOffReason %}
|
||||
{{ disposition.calledOffReason|default('-')|nl2br }}
|
||||
{% else %}
|
||||
{{ disposition.remarks|default('-')|nl2br }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-start space-x-2">
|
||||
@@ -197,6 +201,16 @@
|
||||
{{ icon('upload') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_granted('CALL_OFF', disposition) %}
|
||||
<button type="button"
|
||||
title="Absage"
|
||||
class="text-red-500"
|
||||
hx-get="{{ path('app_administrative_disposition_call_off', { 'uuid': disposition.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ icon('cancel') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_granted('DELETE', disposition) %}
|
||||
<button type="button"
|
||||
class="text-red-500"
|
||||
@@ -225,4 +239,4 @@
|
||||
zurück
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -139,9 +139,9 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<span>{{ assignment.dispositions|length }}/{{ assignment.availableDispositions }}</span>
|
||||
<span>{{ assignment.activeDispositions|length }}/{{ assignment.availableDispositions }}</span>
|
||||
<ul>
|
||||
{% for disposition in assignment.dispositions %}
|
||||
{% for disposition in assignment.activeDispositions %}
|
||||
<li>
|
||||
<div role="button"
|
||||
title="Teamer:innen-Info anzeigen"
|
||||
@@ -251,4 +251,4 @@
|
||||
<a href="{{ path('app_administrative_assignment_create') }}" class="btn">
|
||||
Neu
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}Einsatz absagen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(form) }}
|
||||
<h2 class="font-bold text-lg pb-4">
|
||||
Teamer {{ disposition.teamer }}
|
||||
</h2>
|
||||
<div class="flex flex-col space-y-4 pb-4">
|
||||
{{ form_row(form.calledOffBy) }}
|
||||
{{ form_row(form.calledOffReason) }}
|
||||
{{ form_row(form.sendNotification) }}
|
||||
</div>
|
||||
<button type="submit" class="btn">
|
||||
Absagen
|
||||
</button>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends 'email/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>
|
||||
Hallo aus Köln,
|
||||
</h1>
|
||||
<p>
|
||||
leider mussten wir deinen Einsatz '{{ assignment.destination }}' absagen, sodass dein Einsatz
|
||||
nicht stattfinden kann:
|
||||
</p>
|
||||
<p>
|
||||
{{ reason | nl2br }}
|
||||
</p>
|
||||
<p>
|
||||
Bei Fragen melde dich gerne unter <a href="mailto:[email protected]">[email protected]</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('app_teamer_index') }}" class="button">
|
||||
Zum Portal
|
||||
</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -12,6 +12,7 @@ label:
|
||||
confirmed: bestätigt
|
||||
ended: beendet
|
||||
completed: abgeschlossen
|
||||
called_off: abgesagt
|
||||
document:
|
||||
type:
|
||||
invoice: Honorarnote
|
||||
|
||||
Reference in New Issue
Block a user