wip: implement invoice generator for teamers

This commit is contained in:
Björn Fromme committed 2026-03-03 09:18:24 +01:00
1 parent 6c608063ad
commit bbdd887dc2
25 files changed
+1584 -21

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<style>
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; color: #111827; }
h1 { font-size: 20px; margin: 0 0 10px; }
.muted { color: #6b7280; }
.section { margin-top: 16px; }
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
th, td { border: 1px solid #d1d5db; padding: 6px; text-align: left; }
th { background: #f3f4f6; }
.right { text-align: right; }
.total-row td { font-weight: bold; }
</style>
</head>
<body>
<h1>Honorarnote</h1>
<div class="muted">Einsatz: {{ assignment.destination }}</div>
<div class="muted">Teamer: {{ teamer }}</div>
<div class="muted">Datum: {{ "now"|date("d.m.Y") }}</div>
<div class="section">
<table>
<thead>
<tr>
<th>Bezeichnung</th>
<th class="right">Betrag</th>
<th class="right">Anzahl</th>
<th class="right">Zeilensumme</th>
</tr>
</thead>
<tbody>
{% for item in invoice.items %}
<tr>
<td>{{ item.labelSnapshot }}</td>
<td class="right">{{ item.unitPriceCentsSnapshot|format_money }}</td>
<td class="right">{{ item.quantity }}</td>
<td class="right">{{ item.lineTotalCents|format_money }}</td>
</tr>
{% endfor %}
<tr class="total-row">
<td colspan="3" class="right">Gesamt</td>
<td class="right">{{ invoice.totalCents|format_money }}</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,94 @@
{% macro collectionRow(form, disposition, refreshPath) %}
{% set inputAttr = {
'hx-post': refreshPath,
'hx-target': '#invoice-editor-form',
'hx-swap': 'outerHTML',
'hx-sync': 'closest form:replace',
'hx-trigger': 'focusout, change',
} %}
<div {{ stimulus_target('form-collection', 'field') }} class="grid lg:grid-cols-12 gap-3 items-end border border-gray-200 rounded p-3">
<div class="lg:col-span-4">
{{ form_row(form.feeId, { attr: inputAttr }) }}
</div>
<div class="lg:col-span-3">
{{ form_row(form.label, { attr: inputAttr }) }}
</div>
<div class="lg:col-span-2">
{{ form_row(form.unitPriceCents, { attr: inputAttr }) }}
</div>
<div class="lg:col-span-2">
{{ form_row(form.quantity, { attr: inputAttr }) }}
</div>
<div class="lg:col-span-1 flex justify-end pb-2">
<button type="button" {{ stimulus_action('form-collection', 'removeItem') }}>
{{ icon('delete', 'w-5 h-5 pointer-events-none text-red-500') }}
</button>
</div>
</div>
{% endmacro %}
{% set savePath = path('app_teamer_disposition_invoice_save', { uuid: disposition.uuid }) %}
{% set submitPath = path('app_teamer_disposition_invoice_submit', { uuid: disposition.uuid }) %}
{% set refreshPath = path('app_teamer_disposition_invoice_refresh', { uuid: disposition.uuid }) %}
<div id="invoice-editor-form">
{{ form_start(form, {
attr: {
'hx-post': savePath,
'hx-target': '#invoice-editor-form',
'hx-swap': 'outerHTML'
}
}) }}
<div class="pb-4">
<twig:MessageBox message="Bitte erfasse deine Honorarnote digital. Du kannst Zwischenschritte als Entwurf speichern." type="info"/>
</div>
{% if disposition.invoiceDraft and disposition.invoiceDraft.status == constant('App\\Entity\\DispositionInvoice::STATUS_REJECTED') %}
<div class="pb-4">
<twig:MessageBox message="Deine Honorarnote wurde abgelehnt. Bitte überarbeite sie." type="error"/>
{% if disposition.invoiceDraft.rejectionComment %}
<div class="pt-2 text-sm">
<strong>Begründung:</strong> {{ disposition.invoiceDraft.rejectionComment|nl2br }}
</div>
{% endif %}
</div>
{% endif %}
<div {{ stimulus_controller('form-collection', { 'prototype': _self.collectionRow(form.items.vars.prototype, disposition, refreshPath)|json_encode }) }}>
<div {{ stimulus_target('form-collection', 'fields') }} class="flex flex-col space-y-3 pb-4">
{% do form.items.setRendered %}
{%- for item in form.items -%}
{{- _self.collectionRow(item, disposition, refreshPath) -}}
{%- endfor -%}
</div>
<div class="flex justify-end pb-4">
<button type="button" class="btn btn--secondary" {{ stimulus_action('form-collection', 'addItem') }}>
Position hinzufügen
</button>
</div>
{{ form_errors(form.items) }}
</div>
<div class="pb-4 text-right font-bold">
Gesamt: {{ dto.totalCents|format_money }}
</div>
<div class="pb-4">
{{ form_row(form.confirmFinal) }}
</div>
<div class="flex items-center space-x-2">
<button type="submit" class="btn" formaction="{{ savePath }}">
Entwurf speichern
</button>
<button type="submit" class="btn" formaction="{{ submitPath }}">
Final einreichen
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
+29 -21
View File
@@ -95,28 +95,36 @@
{# New invoice #}
{% if workflow_can(disposition, 'upload_invoice') %}
{% set formAttr = stimulus_controller('form-upload-guard')|stimulus_action('form-upload-guard', 'guard', 'submit') %}
{{ form_start(form, { 'attr': formAttr.toArray() }) }}
<div class="flex flex-col space-y-4">
<a href="{{ path('app_teamer_disposition_invoicepdf', { 'uuid': disposition.uuid }) }}" target="_blank" class="inline-block">
<img src="{{ asset('build/images/invoice_thumb.jpg') }}" alt="Honorarnote" class="block w-32 h-auto shadow-md">
</a>
<div>
Bitte lade sie ausgefüllt und unterschrieben hier wieder hoch:
{% if invoice_editor_enabled %}
<div
hx-get="{{ path('app_teamer_disposition_invoice_editor', { 'uuid': disposition.uuid }) }}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML"
>
{% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8' } %}
</div>
{% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path('_uploader_upload_invoice'),
'accepted_files': 'image/jpg,image/jpeg,application/pdf',
} %}
{% if form.confirmInvoiceData is defined %}
{{ form_row(form.confirmInvoiceData) }}
{% endif %}
<button type="submit" class="btn" {{ stimulus_target('form-upload-guard', 'submit') }}>
Aktualisieren
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% else %}
{% set formAttr = stimulus_controller('form-upload-guard')|stimulus_action('form-upload-guard', 'guard', 'submit') %}
{{ form_start(form, { 'attr': formAttr.toArray() }) }}
<div class="flex flex-col space-y-4">
<a href="{{ path('app_teamer_disposition_invoicepdf', { 'uuid': disposition.uuid }) }}" target="_blank" class="inline-block">
<img src="{{ asset('build/images/invoice_thumb.jpg') }}" alt="Honorarnote" class="block w-32 h-auto shadow-md">
</a>
<div>
Bitte lade sie ausgefüllt und unterschrieben hier wieder hoch:
</div>
{% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path('_uploader_upload_invoice'),
'accepted_files': 'image/jpg,image/jpeg,application/pdf',
} %}
<button type="submit" class="btn" {{ stimulus_target('form-upload-guard', 'submit') }}>
Aktualisieren
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endif %}
{% else %}
{% for blocker in workflow_transition_blockers(disposition, 'upload_invoice') %}
<twig:MessageBox message="{{ blocker.message }}"/>