feat: admin editable email templates

addresses #869eg7ptr
This commit is contained in:
Björn Fromme
2026-08-17 10:39:19 +02:00
parent 804dab335f
commit 3e08965e48
49 changed files with 1760 additions and 276 deletions
@@ -0,0 +1,27 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.subject) }}
{{ form_row(form.headline) }}
{{ form_row(form.body) }}
<div class="bg-gray-100 rounded-md p-4 text-sm">
<div class="font-bold pb-2">
Verfügbare Platzhalter
</div>
<ul class="space-y-1">
{% for name, description in definition.placeholders %}
<li>
<code class="font-bold">{{ '{' ~ name ~ '}' }}</code> &ndash; {{ description }}
</li>
{% endfor %}
</ul>
<div class="pt-2">
Platzhalter ohne Wert werden als <code class="font-bold">-</code> ausgegeben.
</div>
</div>
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -0,0 +1,77 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Einstellungen - E-Mail-Texte{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-2">
E-Mail-Texte
</h1>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
E-Mail
</th>
<th>
Betreff
</th>
<th>
Status
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for key, definition in definitions %}
{% set emailText = emailTexts[key]|default(null) %}
<tr>
<td>
{{ definition.label }}
</td>
<td>
{{ emailText ? emailText.subject : definition.defaultSubject }}
</td>
<td>
{% if emailText %}
Angepasst
{% else %}
Standard
{% endif %}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
{% if emailText %}
<button type="button"
class="text-red-500"
title="Auf Standardtext zurücksetzen"
hx-get="{{ path('app_admin_system_email_text_reset', { 'key': key }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('refresh') }}
</button>
{% endif %}
<button type="button"
title="Vorschau anzeigen"
hx-get="{{ path('app_admin_system_email_text_preview', { 'key': key }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('eye') }}
</button>
<button type="button"
title="E-Mail-Text bearbeiten"
hx-get="{{ path('app_admin_system_email_text_edit', { 'key': key }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('edit') }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}{{ definition.label }}{% endblock %}
{% block content %}
{% include 'admin/system/email_text/_form.html.twig' %}
{% endblock %}
@@ -0,0 +1,27 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Vorschau: {{ definition.label }}{% endblock %}
{% block content %}
<div class="pb-4">
<span class="font-bold">Betreff:</span> {{ text.subject }}
</div>
{# The renderer escaped everything before it added a tag, see RenderedEmailTextDto. #}
<div class="border border-gray-200 rounded-md p-4">
{% if text.headline is not empty %}
<h1 class="text-xl font-bold pb-2">{{ text.headline | raw }}</h1>
{% endif %}
{# The paragraphs come from the mail body, where email/layout.html.twig styles every
<p> with padding-bottom:16px - pb-4 is the same spacing, so the preview matches. #}
<div class="[&>p]:pb-4 [&_a]:underline">
{{ text.bodyHtml | raw }}
</div>
<p>
<span class="btn inline-block">Zum Portal</span>
</p>
</div>
<div class="pt-4 text-sm text-gray-500">
Platzhalter sind als <code>[name]</code> dargestellt und werden beim Versand durch
die echten Werte ersetzt. Logo und Fußzeile der E-Mail sind hier nicht abgebildet.
</div>
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block content %}
<div class="pb-4">
Möchtest du den Text der E-Mail <em>{{ definition.label }}</em> wirklich auf den
Standardtext zurücksetzen? Deine Änderungen gehen dabei verloren.
</div>
{% endblock %}
{% block button_confirm %}Zurücksetzen{% endblock %}
@@ -1,39 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Herzlichen Glückwunsch!
</h1>
<p>
<strong>
Dein Einsatz <a href="{{ url('app_teamer_disposition_detail', { 'uuid': disposition.uuid }) }}">{{ disposition.assignment.destination }}</a>
wurde angenommen!
</strong>
</p>
{% if disposition.specialAgreements %}
<p>
<strong>Zusätzliche Absprachen:</strong>
<br>
{{ disposition.specialAgreements | nl2br }}
</p>
{% endif %}
<p>
Schau einmal in das <a href="{{ url('app_teamer_disposition_detail', { 'uuid': disposition.uuid }) }}">My E&amp;P-Team Portal</a>,
um die Einsatzdetails einzusehen und deinen Honorarvertrag zu unterschreiben.
</p>
<p>
Mit dem Erhalt dieser E-Mail hast du 7 Tage Zeit deinen Einsatz zu bestätigen, indem du den unterschriebenen
Vertrag hochlädst. Ist diese Frist vergangen, wird der Einsatz für deine Teamkolleg:innen freigeschaltet.
</p>
<p>
Schön, dass du dabei bist und ganz viel Spaß in den Bergen! 😊
</p>
<p>
Bitte wende dich bei Rückfragen an <a href="mailto:[email protected]">[email protected]</a>.
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
@@ -1,20 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Hallo aus Köln,
</h1>
<p>
leider konnte deine Bewerbung für den Einsatz '{{ application.assignment.destination }}' nicht angenommen
werden. Dies kann unterschiedliche Gründe haben, wie bspw. Unpassende Buszustiege, zu späte Bewerbung,
unpassendes Jobprofil, zu viele andere Bewerbungen o.Ä.
</p>
<p>
Bewirb dich doch gerne auf weitere Einsätze, vielleicht hast du diesmal Glück! 😊
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
@@ -1,22 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Hallo aus Köln,
</h1>
<p>
leider mussten wir die Reise zu deinem Einsatz '{{ assignment.destination }}' absagen, sodass dein Einsatz
nicht stattfinden kann.
</p>
<p>
Bitte entschuldige die Umstände!
</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 %}
@@ -7,9 +7,4 @@
<p>
Es wurde ein neuer Honorarvertrag von {{ disposition.teamer }} hochgeladen, schau ihn dir an.
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
+2
View File
@@ -9,3 +9,5 @@
wurde freigegeben.
</p>
{% endblock %}
{% block button %}{% endblock %}
@@ -1,22 +0,0 @@
{% 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 %}
@@ -1,22 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Hallo aus Köln,
</h1>
<p>
leider wurde {{ documentTypeLabel }} für den Einsatz {{ disposition.assignment.destination}} mit folgender
Begründung abgelehnt:
</p>
<p>
{{ comment | nl2br }}
</p>
<p>
Überarbeite das Dokument bitte und lade es erneut hoch.
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
+15
View File
@@ -0,0 +1,15 @@
{% extends 'email/layout.html.twig' %}
{# Every string on `text` was escaped by App\Email\EmailTextRenderer before the <strong>,
<p> and <a> tags in it were introduced, which is what makes |raw safe here. Do not
pass anything into this template that did not come out of that renderer.
The closing button is rendered by the layout. #}
{% block body %}
{% if text.headline is not empty %}
<h1>
{{ text.headline | raw }}
</h1>
{% endif %}
{{ text.bodyHtml | raw }}
{% endblock %}
@@ -7,9 +7,4 @@
<p>
Es wurde eine neue Honorarnote von {{ disposition.teamer }} hochgeladen, gib diese bitte frei.
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
+5
View File
@@ -220,6 +220,11 @@
<a href="#" class="button"> Button </a>
</p>
<p class="small"> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloribus, eveniet! </p> {% endblock %}
{% block button %}
<p>
<a href="{{ url('app_index') }}" class="button">Zum Portal</a>
</p>
{% endblock %}
</div>
</td>
</tr>
@@ -1,23 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Hallo aus Köln,
</h1>
<p>
bitte denke daran, deinen ausgefüllten Honorarvertrag für folgenden Einsatz hochzuladen. Vorher können wir nicht
fest mit dir planen und behalten uns vor den Einsatz anderweitig zu vergeben.
</p>
<p>
<strong>
<a href="{{ url('app_teamer_disposition_detail', { 'uuid': disposition.uuid }) }}">
{{ disposition.assignment.destination}}
</a>.
</strong>
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
@@ -1,31 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
{% set destination = disposition.assignment.destination %}
<h1>
Hallo aus Köln,
</h1>
<p>
dein Einsatz als {{ disposition.assignment.jobProfile.name }}, {{ destination.product }} vom
{{ destination.dateFrom | date('d.m.Y') }} bis {{ destination.dateTo | date('d.m.Y') }} beginnt
<strong>in {{ diffInDays }} Tagen</strong>.
</p>
<p>
Wir hoffen, dass du ich schon freust und wünschen dir viel Spaß und Erfolg! Finale Infos erhältst du, falls noch
nicht geschehen, ein paar Tage vor deinem Einsatz von deinen zuständigen Haus- oder Reisemanager:innen.
</p>
<p>
Bitte denk daran, deine <strong>ausgefüllte Honorarnote bis zu 14 Tage nach deinem Einsatz</strong> in My
E&amp;P Team hochzuladen.
</p>
<p>
Liebe Grüße,
<br>
dein Team Personalabteilung
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
@@ -1,17 +0,0 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Hallo aus Köln,
</h1>
<p>
vielen Dank für deinen Einsatz {{ disposition.assignment.destination}}. Bitte lade deine Honorarnote innerhalb
der nächsten 14 Tage hoch, damit wir dein Honorar bearbeiten können. Solltest du deine Honorarnote zu spät
hochladen, können wir sie eventuell nicht mehr annehmen.
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
@@ -4,9 +4,4 @@
<h1>
Reminder zu ausstehenden Feedbacks
</h1>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}
-5
View File
@@ -4,9 +4,4 @@
<p>
{{ message | nl2br }}
</p>
<p>
<a href="{{ url('app_teamer_index') }}" class="button">
Zum Portal
</a>
</p>
{% endblock %}