feat: import work hours from SaaS and exchange signed time sheet

This commit is contained in:
fromme committed 2026-09-26 12:13:59 +02:00
1 parent 7cf39850e6
commit 1a7c272414
73 files changed
+10488 -72

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
{% extends 'admin/layout.html.twig' %}
{% block title %}Arbeitszeiten{% endblock %}
{% block content %}
<div class="flex items-start justify-between pb-4">
<h1 class="text-2xl font-bold">
Arbeitszeiten
</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="Arbeitszeiten filtern"
hx-get="{{ path('app_common_work_month_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_work_month_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, 'Monat', 'work_month.year') }}</th>
<th class="text-right">Stunden</th>
<th>Status</th>
<th>Nachweis</th>
<th></th>
</tr>
</thead>
<tbody>
{% for workMonth in pagination %}
<tr>
<td>
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': workMonth.teamer.uuid, 'r': return_url() }) }}">
<span class="whitespace-nowrap">{{ workMonth.teamer.lastName }}</span>
</a>
</td>
<td>
<span class="whitespace-nowrap">{{ workMonth.teamer.firstName }}</span>
</td>
<td>
<span class="whitespace-nowrap">{{ workMonth.periodLabel }}</span>
</td>
<td class="text-right">
<span class="whitespace-nowrap">{{ workMonth.totalMinutes|duration_hm }}</span>
{% if workMonth.hasMismatch %}
<div class="text-xs text-red-600 whitespace-nowrap"
title="Die SaaS meldet inzwischen andere Stunden für diesen bereits eingereichten Monat.">
jetzt {{ workMonth.mismatchTotalMinutes|duration_hm }}
</div>
{% endif %}
{% if workMonth.warnings is not empty %}
<div class="text-xs text-amber-700 whitespace-nowrap"
title="{{ workMonth.warnings|join(' | ') }}">
{{ workMonth.warnings|length }} Hinweis(e)
</div>
{% endif %}
</td>
<td>
<span class="upload-status-badge {{ workMonth.status.badgeClass }}">
{{ workMonth.status.label }}
</span>
</td>
<td>
{% if workMonth.currentDocument %}
<a href="{{ path('app_common_download', { 'uuid': workMonth.currentDocument.uuid }) }}"
class="flex items-center space-x-1">
{{ icon('download', 'w-4 h-4 shrink-0') }}
<span class="whitespace-nowrap">{{ workMonth.submittedAt|date('d.m.Y') }}</span>
</a>
{% else %}
<span class="text-gray-400">–</span>
{% endif %}
</td>
<td>
<twig:DropDown>
<a href="{{ path('app_admin_work_month_pdf', { 'uuid': workMonth.uuid }) }}"
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
role="menuitem"
tabindex="-1"
target="_blank">
Nachweis ansehen
</a>
{% if workMonth.status.value == 'submitted' %}
<a href="{{ path('app_admin_work_month_review', { 'uuid': workMonth.uuid, 'r': return_url() }) }}"
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_work_month_review', { 'uuid': workMonth.uuid, 'r': return_url() }) }}"
hx-target="body"
hx-swap="beforeend">
Prüfen
</a>
{% endif %}
<form method="post" action="{{ path('app_admin_work_month_sync', { 'uuid': workMonth.uuid, 'r': return_url() }) }}">
<button type="submit"
class="w-full text-left text-gray-700 hover:text-primary block px-4 py-2 text-sm"
role="menuitem"
tabindex="-1">
Erneut abrufen
</button>
</form>
{% if workMonth.status.value in ['submitted', 'approved'] %}
<form method="post" action="{{ path('app_admin_work_month_unlock', { 'uuid': workMonth.uuid, 'r': return_url() }) }}">
<button type="submit"
class="w-full text-left text-gray-700 hover:text-primary block px-4 py-2 text-sm"
role="menuitem"
tabindex="-1"
onclick="return confirm('Der Monat wird wieder geöffnet und muss neu unterschrieben werden. Fortfahren?')">
Wieder öffnen
</button>
</form>
{% endif %}
</twig:DropDown>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-gray-500">
Keine Arbeitszeiten gefunden.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{{ knp_pagination_render(pagination) }}
{% endblock %}
@@ -0,0 +1,49 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Arbeitszeitnachweis prüfen{% endblock %}
{% block content %}
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }}
<div class="pb-4">
<div class="font-bold">
{{ workMonth.teamer }} &middot; {{ workMonth.periodLabel }}
</div>
<div class="text-sm text-gray-500">
{{ workMonth.totalMinutes|duration_hm }} Stunden an {{ workMonth.workedDays }} Tagen,
eingereicht am {{ workMonth.submittedAt|date('d.m.Y') }}
</div>
{% if workMonth.hasMismatch %}
<div class="text-sm text-red-600 pt-2">
Achtung: die SaaS meldet für diesen Monat inzwischen
{{ workMonth.mismatchTotalMinutes|duration_hm }} Stunden.
</div>
{% endif %}
</div>
<div class="flex flex-col space-y-1 pb-4">
{% if workMonth.currentDocument %}
<a href="{{ path('app_common_download', { 'uuid': workMonth.currentDocument.uuid }) }}"
class="flex items-center space-x-1">
{{ icon('download') }}
<span class="flex-shrink-0">Hochgeladenen Nachweis ansehen</span>
</a>
{% endif %}
<a href="{{ path('app_admin_work_month_pdf', { 'uuid': workMonth.uuid }) }}"
class="flex items-center space-x-1"
target="_blank">
{{ icon('pdf') }}
<span class="flex-shrink-0">Nachweis aus den Daten erzeugen</span>
</a>
</div>
<div class="flex flex-col space-y-2 pb-4">
{{ form_row(form.status) }}
{{ form_row(form.comment) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}