feat: all-season assignments

This commit is contained in:
Björn Fromme authored and Björn Fromme committed 2026-09-26 12:13:16 +02:00
1 parent 675cec1b69
commit cf876ed224
119 files changed
+6194 -294

No files matched your search

+2 -2
View File
@@ -93,7 +93,7 @@
{% if recipients.eligibleCount > 0 %}
<button type="button"
class="btn"
hx-post="{{ path('app_admin_teamer_mailing_confirm') }}"
hx-post="{{ path('app_admin_teamer_mailing_confirm', { 'scope': scope }) }}"
hx-include="closest form"
hx-target="body"
hx-swap="beforeend">
@@ -105,7 +105,7 @@
Vorschau an mich senden
</button>
{% if hasDraft %}
<a href="{{ path('app_admin_teamer_mailing_discard') }}" class="btn btn--secondary">
<a href="{{ path('app_admin_teamer_mailing_discard', { 'scope': scope }) }}" class="btn btn--secondary">
Entwurf verwerfen
</a>
{% endif %}
@@ -41,4 +41,4 @@
{# without a complete mail there is nothing to confirm, the button only leads back to the draft #}
{% block button_confirm %}{{ sendable ? 'Jetzt senden' : 'Zurück zum Entwurf' }}{% endblock %}
{% block button_attribute %}{% if sendable %}hx-post="{{ path('app_admin_teamer_mailing_send') }}" hx-include="#mailing-confirm-fields"{% else %}{{ stimulus_action('modal', 'close') }}{% endif %}{% endblock %}
{% block button_attribute %}{% if sendable %}hx-post="{{ path('app_admin_teamer_mailing_send', { 'scope': scope }) }}" hx-include="#mailing-confirm-fields"{% else %}{{ stimulus_action('modal', 'close') }}{% endif %}{% endblock %}
+3
View File
@@ -20,6 +20,9 @@
<div class="pb-4">
{{ form_widget(form.status) }}
</div>
<div class="pb-4">
{{ form_row(form.allSeason) }}
</div>
<div class="pb-4">
{{ form_row(form.allowOverlappingApplications) }}
</div>
@@ -0,0 +1,17 @@
{# Swapped in place by its own route, so the id and the outerHTML swap have to stay. #}
<div id="season-done-teamer" class="flex items-start space-x-2">
<input type="checkbox"
id="season-done-teamer-input"
class="mt-1 text-primary"
{{ seasonAssignment.doneTeamer ? 'checked' : '' }}
hx-post="{{ path('app_administrative_season_assignment_done_teamer', { 'uuid': seasonAssignment.uuid }) }}"
hx-target="#season-done-teamer"
hx-swap="outerHTML">
<label for="season-done-teamer-input">
<span class="font-bold">Teamer:in erledigt</span>
<span class="block text-sm text-gray-500">
Reiner Vermerk, die wöchentliche Erinnerung läuft weiter,
solange ein Punkt der Checkliste mit Rückgabe offen ist.
</span>
</label>
</div>
@@ -0,0 +1,25 @@
{{ form_start(form) }}
<div class="flex flex-col space-y-4 pb-8">
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
{{ form_row(form.teamer) }}
{{ form_row(form.hotelCode) }}
</div>
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
</div>
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
{{ form_row(form.jobProfile) }}
{{ form_row(form.remarks) }}
</div>
</div>
<div class="flex items-center space-x-2">
<button type="submit" class="btn">
Speichern
</button>
<a href="{{ returnUrl }}" class="btn btn--secondary">
Abbrechen
</a>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
@@ -0,0 +1,63 @@
{# One checklist row. Rendered inside the table on the detail page and again on its own by the
toggle route, which swaps exactly this element - hence the id and the outerHTML swap. #}
<tr id="season-task-{{ row.task.uuid }}" class="border-b border-gray-200">
<td class="py-2 pr-4 align-top">
<div class="font-bold">{{ row.definition.label }}</div>
{% if row.definition.provides == constant('App\\Config\\SeasonTaskProvision::STANDARD') %}
{% if row.document is not null %}
<a href="{{ path('app_common_download', { 'uuid': row.document.uuid }) }}"
class="text-sm underline">
{{ row.document.displayName ?? row.document.originalFilename }}
</a>
{% else %}
<div class="text-sm text-gray-500">kein Dokument hinterlegt</div>
{% endif %}
{% elseif row.definition.provides == constant('App\\Config\\SeasonTaskProvision::INDIVIDUAL') %}
<div class="text-sm text-gray-500">individuelles Dokument</div>
{% endif %}
</td>
<td class="py-2 pr-4 align-top whitespace-nowrap">
{{ row.deadline1 is not null ? row.deadline1 | date('d.m.Y') : '-' }}
{% if row.task.deadlineOverride1 is not null %}
<span class="text-xs text-gray-500">(angepasst)</span>
{% endif %}
</td>
<td class="py-2 pr-4 align-top whitespace-nowrap">
{{ row.deadline2 is not null ? row.deadline2 | date('d.m.Y') : '-' }}
{% if row.task.deadlineOverride2 is not null %}
<span class="text-xs text-gray-500">(angepasst)</span>
{% endif %}
</td>
<td class="py-2 pr-4 align-top text-center">
<input type="checkbox"
class="text-primary"
{{ row.task.uploaded ? 'checked' : '' }}
aria-label="Dokument erhalten"
hx-post="{{ path('app_administrative_season_assignment_task_toggle', { 'uuid': row.task.uuid, 'field': 'uploaded' }) }}"
hx-target="#season-task-{{ row.task.uuid }}"
hx-swap="outerHTML">
</td>
<td class="py-2 pr-4 align-top text-center">
<input type="checkbox"
class="text-primary"
{{ row.task.done ? 'checked' : '' }}
aria-label="Erledigt"
hx-post="{{ path('app_administrative_season_assignment_task_toggle', { 'uuid': row.task.uuid, 'field': 'done' }) }}"
hx-target="#season-task-{{ row.task.uuid }}"
hx-swap="outerHTML">
{% if row.task.done and row.task.doneAt is not null %}
<div class="text-xs text-gray-500 whitespace-nowrap">
{{ row.task.doneAt | date('d.m.Y') }}{% if row.task.doneBy is not null %}, {{ row.task.doneBy }}{% endif %}
</div>
{% endif %}
</td>
<td class="py-2 align-top text-right">
<button type="button"
class="text-sm underline"
hx-get="{{ path('app_administrative_season_assignment_task_deadline', { 'uuid': row.task.uuid, 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
Fristen
</button>
</td>
</tr>
@@ -0,0 +1,10 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Saisoneinsatz anlegen{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Saisoneinsatz anlegen
</h1>
{% include 'administrative/season_assignment/_form.html.twig' %}
{% endblock %}
@@ -0,0 +1,161 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Saisoneinsatz{% endblock %}
{% block content %}
<div class="flex items-start justify-between pb-4">
<h1 class="text-2xl font-bold">
Saisoneinsatz
<span class="block text-base font-normal text-gray-500">
{{ seasonAssignment | season_assignment_label }}
</span>
</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_season_assignment_edit', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}" class="btn btn--small">
bearbeiten
</a>
<a href="{{ returnUrl }}" class="btn btn--small btn--secondary">
zurück
</a>
</div>
</div>
{% set teamer = seasonAssignment.teamer %}
<div class="flex flex-col lg:flex-row lg:space-x-8">
<div class="flex-1 grid lg:grid-cols-2 gap-x-8 gap-y-4 pb-8">
<div>
<h4 class="font-bold pb-2">Teamer:in</h4>
{% if teamer is not null %}
<ul class="space-y-2">
<li>
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}">
{{ teamer }}
</a>
</li>
{% if teamer.communication.email %}
<li>
<a href="mailto:{{ teamer.communication.email }}" class="flex items-center space-x-2">
{{ icon('mail', 'w-4 h-4') }}
<span class="flex-0 underline">{{ teamer.communication.email }}</span>
</a>
</li>
{% endif %}
{% if teamer.communication.mobile %}
<li>
<a href="tel:{{ teamer.communication.mobile }}" class="flex items-center space-x-2">
{{ icon('phone', 'w-4 h-4') }}
<span class="flex-0 underline">{{ teamer.communication.mobile }}</span>
</a>
</li>
{% endif %}
</ul>
{% else %}
<span class="text-gray-500">noch nicht zugewiesen</span>
{% endif %}
</div>
<ul class="space-y-4">
<li>
<h4 class="font-bold pb-2">Jobprofil</h4>
{{ seasonAssignment.jobProfile.name }}
</li>
<li>
<h4 class="font-bold pb-2">Haus</h4>
{{ seasonAssignment.hotelCode | house_label }}
</li>
<li>
<h4 class="font-bold pb-2">Zeitraum</h4>
{{ seasonAssignment.dateFrom | date('d.m.Y') }} - {{ seasonAssignment.dateTo | date('d.m.Y') }}
</li>
</ul>
</div>
{% if teamer is not null and teamer.photo is not null %}
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}">
<img src="{{ asset(teamer.photo | upload_filename_full | imagine_filter('profile')) }}"
class="w-48 h-auto border-2 border-primary"
alt="{{ teamer.firstName }}">
</a>
{% endif %}
</div>
<div class="pb-8">
<h2 class="text-xl font-bold pb-2">
Checkliste
</h2>
<div class="data-table-wrapper overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b-2 border-gray-300">
<th class="py-2 pr-4">Dokument/ToDo</th>
<th class="py-2 pr-4">Frist 1</th>
<th class="py-2 pr-4">Frist 2</th>
<th class="py-2 pr-4 text-center">erhalten</th>
<th class="py-2 pr-4 text-center">erledigt</th>
<th class="py-2"></th>
</tr>
</thead>
<tbody>
{% for row in checklist %}
{% include 'administrative/season_assignment/_task_row.html.twig' with { 'row': row } only %}
{% endfor %}
</tbody>
</table>
</div>
<p class="pt-2 text-sm text-gray-500">
Nur <em>erledigt</em> schließt einen Punkt ab und beendet die Erinnerungen.
<em>erhalten</em> ist ein reiner Vermerk.
</p>
<div class="pt-4">
{% include 'administrative/season_assignment/_done_teamer.html.twig' %}
</div>
</div>
<div class="pb-8">
<div class="flex items-center justify-between pb-2">
<h2 class="text-xl font-bold pb-2">
Dokumente
</h2>
<button type="button"
class="btn btn--small"
hx-get="{{ path('app_administrative_season_assignment_document_upload', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
hochladen
</button>
</div>
{% if seasonAssignment.documents | length > 0 %}
<ul class="divide-y divide-gray-200">
{% for document in seasonAssignment.documents %}
<li class="flex items-center justify-between py-2">
<div>
<a href="{{ path('app_common_download', { 'uuid': document.uuid }) }}" class="underline">
{{ document.originalFilename }}
</a>
<div class="text-sm text-gray-500">
{{ document.typeLabel }}, hochgeladen am {{ document.createdAt | date('d.m.Y') }}
</div>
</div>
<button type="button"
class="text-sm text-red-500"
hx-get="{{ path('app_administrative_season_assignment_document_delete', { 'uuid': document.uuid, 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
löschen
</button>
</li>
{% endfor %}
</ul>
{% else %}
<div class="text-gray-500">keine Dokumente vorhanden</div>
{% endif %}
</div>
<div class="pb-8">
<h4 class="font-bold pb-2">Anmerkungen intern</h4>
{% if seasonAssignment.remarks is not empty %}
<div class="whitespace-pre-line">{{ seasonAssignment.remarks }}</div>
{% else %}
<div class="text-gray-500">-</div>
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Saisoneinsatz bearbeiten{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
Saisoneinsatz bearbeiten
</h1>
{% include 'administrative/season_assignment/_form.html.twig' %}
{% endblock %}
@@ -0,0 +1,136 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Saisoneinsätze{% endblock %}
{% block content %}
<div class="flex items-start justify-between pb-4">
<h1 class="text-2xl font-bold">
Saisoneinsätze
</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">
<button type="button"
class="{{ html_classes('btn btn--small', { 'btn--secondary': filterDto.active }) }}"
title="Saisoneinsätze filtern"
hx-get="{{ path('app_common_season_assignment_filter', { 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('filter', 'w-4 h-4 shrink-0') }}
{% if filterDto.active %}
<span class="whitespace-nowrap">{{ filterDto.activeFiltersCount }} Filter aktiv</span>
{% else %}
<span>Filter</span>
{% endif %}
</button>
{% if filterDto.active %}
<a href="{{ path('app_common_season_assignment_filter_reset', { 'r': return_url() }) }}" class="btn btn--small btn--secondary">
Reset
</a>
{% endif %}
</div>
</div>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Name', 'teamer.lastName') }}
</th>
<th>
Vorname
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Haus', 'season_assignment.hotelCode') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Zeitraum', 'season_assignment.dateFrom') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Jobprofil', 'job_profile.name') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for seasonAssignment in pagination %}
<tr>
{# an assignment that has not been staffed yet still belongs in the list #}
{% if seasonAssignment.teamer is not null %}
<td>
<a href="{{ path('app_administrative_season_assignment_detail', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}">
<span class="whitespace-nowrap">{{ seasonAssignment.teamer.lastName }}</span>
</a>
</td>
<td>
<a href="{{ path('app_administrative_season_assignment_detail', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}" class="whitespace-nowrap">
{{ seasonAssignment.teamer.firstName }}
</a>
</td>
{% else %}
<td colspan="2">
<a href="{{ path('app_administrative_season_assignment_detail', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}"
class="whitespace-nowrap text-gray-500">
noch nicht zugewiesen
</a>
</td>
{% endif %}
<td>
{{ seasonAssignment.hotelCode | house_label }}
</td>
<td>
<span class="whitespace-nowrap">
{{ seasonAssignment.dateFrom | date('d.m.Y') }} - {{ seasonAssignment.dateTo | date('d.m.Y') }}
</span>
</td>
<td>
{{ seasonAssignment.jobProfile.name }}
</td>
<td>
<twig:DropDown>
<a href="{{ path('app_administrative_season_assignment_detail', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}"
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
role="menuitem"
tabindex="-1">
Details
</a>
<a href="{{ path('app_administrative_season_assignment_edit', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}"
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
role="menuitem"
tabindex="-1">
bearbeiten
</a>
<button type="button"
class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_administrative_season_assignment_delete', { 'uuid': seasonAssignment.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
löschen
</button>
</twig:DropDown>
{% if seasonAssignment.deleted %}
<div class="py-1 px-2 mt-2 text-xs bg-gray-500 text-white">
gel. am {{ seasonAssignment.deletedAt | date('d.m.Y') }}
</div>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6">
Keine Daten...
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
<a href="{{ path('app_administrative_season_assignment_create', { 'r': return_url() }) }}" class="btn">
Neu
</a>
{% endblock %}
@@ -0,0 +1,13 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block content %}
<div>
{% if seasonAssignment.teamer is not null %}
Möchtest du den Saisoneinsatz von <em>{{ seasonAssignment.teamer }}</em>
in <em>{{ seasonAssignment | season_assignment_label }}</em> wirklich löschen?
{% else %}
Möchtest du den noch nicht zugewiesenen Saisoneinsatz
in <em>{{ seasonAssignment | season_assignment_label }}</em> wirklich löschen?
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,7 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block content %}
<div>
Möchtest du das Dokument <em>{{ document.originalFilename }}</em> wirklich löschen?
</div>
{% endblock %}
@@ -0,0 +1,26 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Dokumente hochladen{% endblock %}
{% block content %}
{% set formAttr = stimulus_controller('form-upload-guard')|stimulus_action('form-upload-guard', 'guard', 'submit') %}
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }|merge(formAttr.toArray()) }) }}
<div class="pb-4">
Lade hier die Dokumente hoch, die
{% if seasonAssignment.teamer is not null %}<em>{{ seasonAssignment.teamer }}</em>{% else %}der:die Teamer:in{% endif %}
für diesen Saisoneinsatz erhält. Sie sind nur für diese Person sichtbar.
</div>
<div class="pb-4">
{% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path('_uploader_upload_season_document_out'),
'max_filesize': 5,
'max_files': 10,
'accepted_files': 'image/jpg,image/jpeg,application/pdf',
} %}
</div>
<button type="submit" class="btn" {{ stimulus_target('form-upload-guard', 'submit') }}>
Dokumente speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,19 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Fristen: {{ task.task.label }}{% endblock %}
{% block content %}
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }}
<div class="flex flex-col space-y-4 pb-8">
<p class="text-sm text-gray-500">
Ohne Eintrag gilt die Frist, die sich aus dem Beginn des Saisoneinsatzes ergibt.
</p>
{{ form_row(form.deadlineOverride1) }}
{{ form_row(form.deadlineOverride2) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,60 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Saisonteam-Dokumente{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-2">
Saisonteam-Dokumente
</h1>
<p class="pb-8 text-gray-500">
Auswahl der Standarddokumente für das Saisonteam.
</p>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th>ToDo</th>
<th>Dokument</th>
<th></th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.task.label }}</td>
<td>
{% if row.document is not null %}
<a href="{{ path('app_common_download', { 'uuid': row.document.document.uuid }) }}" class="underline">
{{ row.document.document.displayName ?? row.document.document.originalFilename }}
</a>
{% else %}
<span class="text-gray-500">kein Dokument hinterlegt</span>
{% endif %}
</td>
<td>
<div class="flex items-center space-x-2 justify-end">
{% if row.document is not null %}
<button type="button"
class="text-red-500"
hx-get="{{ path('app_administrative_system_season_task_document_delete', { 'uuid': row.document.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('delete') }}
</button>
{% endif %}
<button type="button"
hx-get="{{ path('app_administrative_system_season_task_document_edit', { 'taskKey': row.task.value }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('edit') }}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,8 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block content %}
<div>
Möchtest du die Zuordnung für <em>{{ seasonTaskDocument.task ? seasonTaskDocument.task.label : seasonTaskDocument.taskKey }}</em>
wirklich entfernen? Das Dokument selbst bleibt erhalten.
</div>
{% endblock %}
@@ -0,0 +1,15 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Dokument für {{ task.label }}{% endblock %}
{% block content %}
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }}
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.document) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,36 @@
{# Shared by both teamer lists. `scope` selects which list's filter the modal edits and
which list a mailing resolves its recipients from; it is null on the general one. #}
{% set scope = scope|default(null) %}
<div class="flex items-start justify-between pb-4">
<h1 class="text-2xl font-bold">
{{ title }}
</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">
<button type="button"
class="{{ html_classes('btn btn--small', { 'btn--secondary': filterDto.active }) }}"
title="Team filtern"
hx-get="{{ path('app_common_teamer_filter', { 'r': return_url(), 'scope': scope }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('filter', 'w-4 h-4 shrink-0') }}
{% if filterDto.active %}
<span class="whitespace-nowrap">{{ filterDto.activeFiltersCount }} Filter aktiv</span>
{% else %}
<span>Filter</span>
{% endif %}
</button>
{% if filterDto.active %}
<a href="{{ path('app_common_teamer_filter_reset', { 'r': return_url(), 'scope': scope }) }}" class="btn btn--small btn--secondary">
Reset
</a>
{% endif %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<a href="{{ path('app_admin_teamer_mailing', { 'r': return_url(), 'scope': scope }) }}"
class="btn btn--small"
title="Mail an die gefilterte Liste schreiben">
{{ icon('mail', 'w-4 h-4 shrink-0') }}
<span>Mailing</span>
</a>
{% endif %}
</div>
</div>
@@ -0,0 +1,155 @@
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th></th>
<th>
{{ knp_pagination_sortable(pagination, 'Name', 'teamer.lastName') }}
</th>
<th>
Vorname
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Status', 'teamer.status') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'E-Mail', 'teamer.communication.email') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Letzter Login', 'user.lastLoginAt') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for teamer in pagination %}
<tr>
<td>
<div class="flex flex-col items-center space-y-2">
{% if teamer.photo %}
<img src="{{ asset(teamer.photo | upload_filename_full | imagine_filter('thumbnail')) }}"
class="object-cover w-12 h-12 rounded-full"
alt="{{ teamer.firstName }}">
{% else %}
<div class="flex flex-col items-center justify-center w-12 h-12 border-2 border-gray-200 rounded-full">
{{ icon('user', 'w-10 h-10 text-gray-200') }}
</div>
{% endif %}
<twig:RatingStars rating="{{ teamer.averageRating }}" :teamer="teamer" />
</div>
</td>
<td>
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}">
<span class="whitespace-nowrap">{{ teamer.lastName }}</span>
</a>
</td>
<td>
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}" class="whitespace-nowrap">
{{ teamer.firstName }}
</a>
</td>
<td>
{{ teamer|teamer_status_label(showAllSeason|default(true)) }}
</td>
<td>
{{ teamer.communication.email }}
</td>
<td>
{% if teamer.user is not null and teamer.user.lastLoginAt is not null %}
{{ teamer.user.lastLoginAt | date('d.m.Y') }}
{% else %}
-
{% endif %}
</td>
<td>
<div class="flex items-center space-x-1 justify-end">
{% if teamer.driverLicenseVerified %}
{{ icon('car', 'w-5 h-5 text-green-500') }}
{% endif %}
{% if not teamer.viewed %}
<button type="button"
hx-post="{{ path('app_administrative_teamer_viewed', { 'uuid': teamer.uuid }) }}"
title="Teamer als gesichtet markieren">
{{ icon('check') }}
</button>
{% endif %}
{% if teamer.deleted %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_restore_account', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend"
title="Account wiederherstellen">
{{ icon('refresh') }}
</button>
{% endif %}
{% elseif teamer.user is not null and teamer.user.disabled %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_enable_user', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('unlocked') }}
</button>
{% endif %}
{% else %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
class="text-red-500"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_disable_user', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('locked') }}
</button>
{% endif %}
{% if is_granted('CAN_IMPERSONATE', teamer.user) %}
<a href="{{ path('app_teamer_index', { '_switch_user': teamer.user.email }) }}" title="Als Teamer:in maskieren">
{{ icon('mask') }}
</a>
{% endif %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
class="text-red-500"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_delete_account', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend"
title="Account löschen">
{{ icon('delete') }}
</button>
{% endif %}
{% endif %}
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}" title="Teamer:innenprofil {{ teamer }}">
{{ icon('user') }}
</a>
</div>
{% if teamer.deleted %}
<div class="py-1 px-2 mt-2 text-xs bg-gray-500 text-white">
gel. am {{ teamer.deletedAt | date('d.m.Y') }}
</div>
{% endif %}
{% if teamer.user is not null and teamer.user.disabled %}
<div class="py-1 px-2 mt-2 text-xs bg-red-500 text-white">
gesp. am {{ teamer.user.disabledAt | date('d.m.Y') }}
</div>
{% endif %}
</td>
</tr>
{% else %}
<td colspan="7">
Keine Daten...
</td>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
+2 -188
View File
@@ -3,192 +3,6 @@
{% block title %}Teamübersicht{% endblock %}
{% block content %}
<div class="flex items-start justify-between pb-4">
<h1 class="text-2xl font-bold">
Teamübersicht
</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">
<button type="button"
class="{{ html_classes('btn btn--small', { 'btn--secondary': filterDto.active }) }}"
title="Team filtern"
hx-get="{{ path('app_common_teamer_filter', { 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('filter', 'w-4 h-4 shrink-0') }}
{% if filterDto.active %}
<span class="whitespace-nowrap">{{ filterDto.activeFiltersCount }} Filter aktiv</span>
{% else %}
<span>Filter</span>
{% endif %}
</button>
{% if filterDto.active %}
<a href="{{ path('app_common_teamer_filter_reset', { 'r': return_url() }) }}" class="btn btn--small btn--secondary">
Reset
</a>
{% endif %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<a href="{{ path('app_admin_teamer_mailing', { 'r': return_url() }) }}"
class="btn btn--small"
title="Mail an die gefilterte Liste schreiben">
{{ icon('mail', 'w-4 h-4 shrink-0') }}
<span>Mailing</span>
</a>
{% endif %}
</div>
</div>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
<thead>
<tr>
<th></th>
<th>
{{ knp_pagination_sortable(pagination, 'Name', 'teamer.lastName') }}
</th>
<th>
Vorname
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Status', 'teamer.status') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'E-Mail', 'teamer.communication.email') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Letzter Login', 'user.lastLoginAt') }}
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for teamer in pagination %}
<tr>
<td>
<div class="flex flex-col items-center space-y-2">
{% if teamer.photo %}
<img src="{{ asset(teamer.photo | upload_filename_full | imagine_filter('profile')) }}"
class="w-12 h-auto rounded-full"
alt="{{ teamer.firstName }}">
{% else %}
<div class="flex flex-col items-center justify-center w-12 h-12 border-2 border-gray-200 rounded-full">
{{ icon('user', 'w-10 h-10 text-gray-200') }}
</div>
{% endif %}
<twig:RatingStars rating="{{ teamer.averageRating }}" :teamer="teamer" />
</div>
</td>
<td>
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}">
<span class="whitespace-nowrap">{{ teamer.lastName }}</span>
</a>
</td>
<td>
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}" class="whitespace-nowrap">
{{ teamer.firstName }}
</a>
</td>
<td>
{{ teamer|teamer_status_label }}
</td>
<td>
{{ teamer.communication.email }}
</td>
<td>
{% if teamer.user is not null and teamer.user.lastLoginAt is not null %}
{{ teamer.user.lastLoginAt | date('d.m.Y') }}
{% else %}
-
{% endif %}
</td>
<td>
<div class="flex items-center space-x-1 justify-end">
{% if teamer.driverLicenseVerified %}
{{ icon('car', 'w-5 h-5 text-green-500') }}
{% endif %}
{% if not teamer.viewed %}
<button type="button"
hx-post="{{ path('app_administrative_teamer_viewed', { 'uuid': teamer.uuid }) }}"
title="Teamer als gesichtet markieren">
{{ icon('check') }}
</button>
{% endif %}
{% if teamer.deleted %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_restore_account', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend"
title="Account wiederherstellen">
{{ icon('refresh') }}
</button>
{% endif %}
{% elseif teamer.user is not null and teamer.user.disabled %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_enable_user', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('unlocked') }}
</button>
{% endif %}
{% else %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
class="text-red-500"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_disable_user', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('locked') }}
</button>
{% endif %}
{% if is_granted('CAN_IMPERSONATE', teamer.user) %}
<a href="{{ path('app_teamer_index', { '_switch_user': teamer.user.email }) }}" title="Als Teamer:in maskieren">
{{ icon('mask') }}
</a>
{% endif %}
{% if is_granted('ROLE_TEAM_ADMIN') %}
<button type="button"
class="text-red-500"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_teamer_delete_account', { 'uuid': teamer.uuid }) }}"
hx-target="body"
hx-swap="beforeend"
title="Account löschen">
{{ icon('delete') }}
</button>
{% endif %}
{% endif %}
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': return_url() }) }}" title="Teamer:innenprofil {{ teamer }}">
{{ icon('user') }}
</a>
</div>
{% if teamer.deleted %}
<div class="py-1 px-2 mt-2 text-xs bg-gray-500 text-white">
gel. am {{ teamer.deletedAt | date('d.m.Y') }}
</div>
{% endif %}
{% if teamer.user is not null and teamer.user.disabled %}
<div class="py-1 px-2 mt-2 text-xs bg-red-500 text-white">
gesp. am {{ teamer.user.disabledAt | date('d.m.Y') }}
</div>
{% endif %}
</td>
</tr>
{% else %}
<td colspan="7">
Keine Daten...
</td>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% include 'administrative/teamer/_header.html.twig' with { 'title': 'Teamübersicht' } %}
{% include 'administrative/teamer/_table.html.twig' %}
{% endblock %}
@@ -244,6 +244,31 @@
</div>
</div>
{% if teamer.allSeason %}
<div>
<h2 class="text-lg font-bold pb-2">
Saisoneinsätze
</h2>
<ul class="list-disc pl-4">
{% for seasonAssignment in teamer.seasonAssignments|filter(seasonAssignment => not seasonAssignment.deleted) %}
<li>
<a href="{{ path('app_administrative_season_assignment_detail', { 'uuid': seasonAssignment.uuid, 'r': return_url() }) }}">
{{ seasonAssignment.hotelCode | house_label }}
</a>
<span class="text-sm block">
{{ seasonAssignment.dateFrom | date('d.m.Y') }} - {{ seasonAssignment.dateTo | date('d.m.Y') }},
{{ seasonAssignment.jobProfile.name }}
</span>
</li>
{% else %}
<li>
<span class="text-sm">Keine Daten</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
@@ -0,0 +1,12 @@
{% extends 'administrative/layout.html.twig' %}
{% block title %}Saisonteamer{% endblock %}
{% block content %}
{% include 'administrative/teamer/_header.html.twig' with {
'title': 'Saisonteamer',
'scope': constant('App\\Service\\Common\\TeamerFilterHandler::SCOPE_ALL_SEASON')
} %}
{# every row here is an all-season teamer, so the marker would only add noise #}
{% include 'administrative/teamer/_table.html.twig' with { 'showAllSeason': false } %}
{% endblock %}
@@ -0,0 +1,19 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Saisoneinsätze filtern{% endblock %}
{% block content %}
{{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML'} }) }}
<div class="flex flex-col space-y-2 pb-2">
{{ form_row(filterForm.teamerName) }}
{{ form_row(filterForm.hotelCodes) }}
{{ form_row(filterForm.jobProfiles) }}
{{ form_row(filterForm.includeDeleted) }}
</div>
<div class="flex items-center space-x-2">
{{ form_widget(filterForm.apply, { 'attr': { 'class': 'btn' } }) }}
{{ form_widget(filterForm.reset, { 'attr': { 'class': 'btn btn--secondary' } }) }}
</div>
{{ form_rest(filterForm) }}
{{ form_end(filterForm) }}
{% endblock %}
@@ -0,0 +1,18 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Fristen für Saison-Teamer:innen-Dokumente erreicht
</h1>
<p>
Für die folgenden Teamer:innen ist heute die Einreichungsfrist für Dokument erreicht:
</p>
{% for teamer, entries in violations %}
<p>
<strong>{{ teamer }}</strong><br>
{% for entry in entries %}
Frist {{ entry.level }}: {{ entry.label }}<br>
{% endfor %}
</p>
{% endfor %}
{% endblock %}
+1
View File
@@ -200,6 +200,7 @@
<input type="text"
class="{{ attr.class }}"
value="{{ form.vars.initial_label }}"
{% if form.vars.placeholder %}placeholder="{{ form.vars.placeholder }}"{% endif %}
{{ stimulus_target('autocomplete', 'input') }}>
<ul class="autocomplete-result-list"></ul>
<input type="hidden"
+100
View File
@@ -0,0 +1,100 @@
{% extends 'teamer/layout.html.twig' %}
{% block title %}Saison-Teamer:in{% endblock %}
{% block content %}
<div class="max-w-screen-md mx-auto">
<h1 class="text-2xl font-bold pb-2">
Saison-Teamer:in
</h1>
<p class="pb-8 text-gray-700">
Für deinen Einsatz im Haus {{ seasonAssignment.hotelCode | house_label }}
({{ seasonAssignment.dateFrom | date('d.m.Y') }} -
{{ seasonAssignment.dateTo | date('d.m.Y') }})
brauchen wir die folgenden Dokumente von dir.
</p>
<h2 class="text-xl font-bold pb-2">
Allgemeine Dokumente
</h2>
<dl class="divide-y divide-gray-100">
{% for row in rows %}
<div class="py-6 first:pt-0 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="font-bold leading-6 text-gray-900 sm:col-span-2">
{{ row.definition.label }}
<div class="text-sm font-normal text-gray-500">
{% if row.deadline1 is not null %}
bis {{ row.deadline1 | date('d.m.Y') }}
{% endif %}
</div>
</dt>
<dd class="mt-1 text-sm leading-6 sm:mt-0">
{% if row.task.done %}
<span class="text-green-600">erledigt</span>
{% else %}
<span class="text-gray-500">offen</span>
{% endif %}
{% if row.document is not null %}
<a href="{{ path('app_common_download', { 'uuid': row.document.uuid }) }}" class="flex items-center space-x-1">
{{ icon('download') }}
<span class="flex-shrink-0">Vorlage herunterladen</span>
</a>
{% elseif row.definition.provides == constant('App\\Config\\SeasonTaskProvision::STANDARD') %}
<div class="text-gray-500">kein Dokument hinterlegt</div>
{% endif %}
</dd>
</div>
{% endfor %}
</dl>
<div class="py-6">
<h2 class="text-xl font-bold pb-2">
Persönliche Dokumente
</h2>
{% set personalDocuments = documents | filter(document => document.type == constant('App\\Entity\\Upload::TYPE_SEASON_DOCUMENT_OUT')) %}
{% if personalDocuments | length > 0 %}
<dl class="divide-y divide-gray-100">
{% for document in personalDocuments %}
<div class="py-2 flex items-center justify-between">
<div>{{ document.displayName | default(document.originalFilename) }}</div>
<a href="{{ path('app_common_download', { 'uuid': document.uuid }) }}" class="flex items-center space-x-1">
{{ icon('download') }}
<span class="flex-shrink-0">Download</span>
</a>
</div>
{% endfor %}
</dl>
{% else %}
<div class="text-gray-500">Zurzeit liegen keine persönlichen Dokumente für dich bereit.</div>
{% endif %}
</div>
<div class="py-6">
<h2 class="text-xl font-bold pb-2">
Deine Uploads
</h2>
{% set returnedDocuments = documents | filter(document => document.type == constant('App\\Entity\\Upload::TYPE_SEASON_DOCUMENT_IN')) %}
{% if returnedDocuments | length > 0 %}
<dl class="divide-y divide-gray-100">
{% for document in returnedDocuments %}
<div class="py-2 flex items-center justify-between">
<div>{{ document.originalFilename }}</div>
<div class="text-sm text-gray-500">{{ document.createdAt | date('d.m.Y') }}</div>
</div>
{% endfor %}
</dl>
{% else %}
<div class="text-gray-500">Du hast noch nichts hochgeladen.</div>
{% endif %}
<button type="button"
class="btn mt-4"
hx-get="{{ path('app_teamer_season_upload', { 'uuid': seasonAssignment.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
Dokumente hochladen
</button>
</div>
</div>
{% endblock %}
@@ -0,0 +1,24 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Dokumente hochladen{% endblock %}
{% block content %}
{% set formAttr = stimulus_controller('form-upload-guard')|stimulus_action('form-upload-guard', 'guard', 'submit') %}
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }|merge(formAttr.toArray()) }) }}
<div class="pb-4">
Lade hier die von dir unterschriebenen Dokumente hoch. Wir prüfen sie und haken die Punkte anschließend ab.
</div>
<div class="pb-4">
{% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path('_uploader_upload_season_document_in'),
'max_filesize': 5,
'max_files': 10,
'accepted_files': 'image/jpg,image/jpeg,application/pdf',
} %}
</div>
<button type="submit" class="btn" {{ stimulus_target('form-upload-guard', 'submit') }}>
Dokumente speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}