Teilnahmerechnungen erstellen

This commit is contained in:
2026-09-03 00:08:46 +02:00
parent a61344395a
commit 9c4c28e566
79 changed files with 4303 additions and 75 deletions
@@ -0,0 +1,42 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Models\DocumentAsset;
use App\Models\DocumentTemplate;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class DocumentAssetDeleteController extends CommonController
{
public function __invoke(string $name): JsonResponse
{
$asset = DocumentAsset::where('name', $name)->first();
// Durchweg 200 mit Status im Body -- wie die übrigen Admin-Endpunkte, damit der Grund im Frontend
// ankommt (der Ajax-Helfer verwirft den Body bei Fehler-Statuscodes).
if ($asset === null) {
return response()->json(['status' => 'error', 'message' => 'Das Bild existiert nicht.']);
}
// Ein noch referenziertes Bild zu löschen würde stillschweigend eine Lücke in jede künftige
// Rechnung reißen -- der Platzhalter löst dann zu einem Leerstring auf.
if ($this->isReferenced($asset->name)) {
return response()->json([
'status' => 'error',
'message' => sprintf('Das Bild wird noch als {asset:%s} in der Vorlage verwendet.', $asset->name),
]);
}
$asset->delete();
return response()->json(['status' => 'success', 'message' => 'Das Bild wurde gelöscht.']);
}
private function isReferenced(string $name): bool
{
return DocumentTemplate::query()
->where('content', 'like', '%{asset:' . $name . '}%')
->exists();
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateDocumentAsset\UpdateDocumentAssetAction;
use App\Domains\Admin\Actions\UpdateDocumentAsset\UpdateDocumentAssetRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DocumentAssetUpdateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new UpdateDocumentAssetAction(new UpdateDocumentAssetRequest(
name: (string) $request->input('name'),
label: $request->input('label'),
file: $request->file('file'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
'token' => $response->asset !== null ? '{asset:' . $response->asset->name . '}' : null,
]);
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\ParticipantInvoice\ParticipantInvoiceTokens;
use App\Models\DocumentAsset;
use App\Models\DocumentTemplate;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class DocumentTemplatesGetController extends CommonController
{
/** Blöcke, die Layout-HTML bzw. CSS enthalten und deshalb im Quelltext-Editor gepflegt werden. */
private const array SOURCE_BLOCKS = [
DocumentTemplate::BLOCK_LAYOUT,
DocumentTemplate::BLOCK_STYLE,
];
private const array BLOCK_LABELS = [
DocumentTemplate::BLOCK_LAYOUT => 'Seitengerüst',
DocumentTemplate::BLOCK_STYLE => 'Gestaltung (CSS)',
'header_sender_return' => 'Rücksendezeile',
'header_recipient' => 'Empfängeranschrift',
'emblem' => 'Emblem',
'logo' => 'Logo',
'sender_data' => 'Absenderangaben',
'subject' => 'Betreff und Referenzdaten',
DocumentTemplate::BLOCK_BODY => 'Rechnungsinhalt',
'footer' => 'Grußformel und Fußnote',
];
public function __invoke(): JsonResponse
{
$blocks = DocumentTemplate::forType(DocumentTemplate::TYPE_PARTICIPANT_INVOICE)
->values()
->map(fn(DocumentTemplate $block): array => [
'block' => $block->block,
'label' => self::BLOCK_LABELS[$block->block] ?? $block->block,
'content' => (string) $block->content,
'editable' => $block->editable,
'source' => in_array($block->block, self::SOURCE_BLOCKS, true),
]);
return response()->json([
'blocks' => $blocks,
'assets' => DocumentAsset::orderBy('name')->get()->map(fn(DocumentAsset $asset): array => [
'name' => $asset->name,
'label' => $asset->label,
'mime' => $asset->mime,
'token' => '{asset:' . $asset->name . '}',
'preview' => $asset->toDataUri(),
]),
'tokenGroups' => ParticipantInvoiceTokens::groups(),
]);
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Inertia\Response;
class DocumentTemplatesPageController extends CommonController
{
public function __invoke(): Response
{
return new InertiaProvider('Admin/DocumentTemplates', [])->render();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\ParticipantInvoice\ParticipantInvoiceTokens;
use App\Models\DocumentTemplate;
use App\Providers\DocumentTemplateRenderProvider;
use App\Providers\PdfGenerateAndDownloadProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* Rendert die Vorlage mit Beispieldaten, ohne sie zu speichern -- damit niemand blind HTML editiert.
*/
class DocumentTemplatesPreviewController extends CommonController
{
public function __invoke(Request $request): Response
{
$html = new DocumentTemplateRenderProvider(
DocumentTemplate::TYPE_PARTICIPANT_INVOICE,
(array) $request->input('blocks', []),
)->render(ParticipantInvoiceTokens::sample());
return response(PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait'), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="vorschau.pdf"',
]);
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateDocumentTemplate\UpdateDocumentTemplateAction;
use App\Domains\Admin\Actions\UpdateDocumentTemplate\UpdateDocumentTemplateRequest;
use App\Models\DocumentTemplate;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DocumentTemplatesUpdateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new UpdateDocumentTemplateAction(new UpdateDocumentTemplateRequest(
documentType: DocumentTemplate::TYPE_PARTICIPANT_INVOICE,
blocks: (array) $request->input('blocks', []),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -15,6 +15,13 @@ class ManagedTenantContactGetController extends CommonController
return response()->json([
'email' => $tenant->email,
'email_finance' => $tenant->email_finance,
'invoice_sender_name' => $tenant->invoice_sender_name,
// Als Platzhalter im Formular: so heisst der Absender, solange nichts eingetragen ist.
'name' => $tenant->name,
'phone' => $tenant->phone,
'address_1' => $tenant->address_1,
'address_2' => $tenant->address_2,
'address_3' => $tenant->address_3,
'postcode' => $tenant->postcode,
'city' => $tenant->city,
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/contact',
@@ -20,6 +20,11 @@ class ManagedTenantContactUpdateController extends CommonController
emailFinance: $request->input('email_finance'),
postcode: $request->input('postcode'),
city: $request->input('city'),
invoiceSenderName: $request->input('invoice_sender_name'),
address1: $request->input('address_1'),
address2: $request->input('address_2'),
address3: $request->input('address_3'),
phone: $request->input('phone'),
));
$response = $action->execute();
@@ -20,6 +20,11 @@ class ManagedTenantTaxGetController extends CommonController
'tax_exemption_reason' => $tenant->tax_exemption_reason,
'tax_exemption_note' => $tenant->tax_exemption_note,
'vat_pricing_mode' => $tenant->vat_pricing_mode,
'tax_number' => $tenant->tax_number,
'vat_id' => $tenant->vat_id,
'invoice_prefix' => $tenant->invoice_prefix,
// In der Mandanten-Verwaltung ist der Nummernkreis änderbar.
'can_edit_invoice_prefix' => true,
'exemption_reasons' => TaxExemptionReason::options(),
'pricing_modes' => VatPricingMode::options(),
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/tax',
@@ -23,6 +23,9 @@ class ManagedTenantTaxUpdateController extends CommonController
exemptionReason: TaxExemptionReason::where('slug', $request->input('tax_exemption_reason'))->first(),
exemptionNote: $request->input('tax_exemption_note'),
vatPricingMode: VatPricingMode::tryFrom((string) $request->input('vat_pricing_mode', 'inclusive')) ?? VatPricingMode::Inclusive,
taxNumber: $request->input('tax_number'),
vatId: $request->input('vat_id'),
invoicePrefix: $request->input('invoice_prefix'),
));
$response = $action->execute();
@@ -13,6 +13,13 @@ class TenantContactGetController extends CommonController
return response()->json([
'email' => $this->tenant->email,
'email_finance' => $this->tenant->email_finance,
'invoice_sender_name' => $this->tenant->invoice_sender_name,
// Als Platzhalter im Formular: so heisst der Absender, solange nichts eingetragen ist.
'name' => $this->tenant->name,
'phone' => $this->tenant->phone,
'address_1' => $this->tenant->address_1,
'address_2' => $this->tenant->address_2,
'address_3' => $this->tenant->address_3,
'postcode' => $this->tenant->postcode,
'city' => $this->tenant->city,
'saveEndpoint' => '/api/v1/admin/tenant/contact',
@@ -18,6 +18,11 @@ class TenantContactUpdateController extends CommonController
emailFinance: $request->input('email_finance'),
postcode: $request->input('postcode'),
city: $request->input('city'),
invoiceSenderName: $request->input('invoice_sender_name'),
address1: $request->input('address_1'),
address2: $request->input('address_2'),
address3: $request->input('address_3'),
phone: $request->input('phone'),
));
$response = $action->execute();
@@ -18,6 +18,11 @@ class TenantTaxGetController extends CommonController
'tax_exemption_reason' => $this->tenant->tax_exemption_reason,
'tax_exemption_note' => $this->tenant->tax_exemption_note,
'vat_pricing_mode' => $this->tenant->vat_pricing_mode,
'tax_number' => $this->tenant->tax_number,
'vat_id' => $this->tenant->vat_id,
'invoice_prefix' => $this->tenant->invoice_prefix,
// Im Self-Service nicht änderbar: das Präfix bestimmt den Nummernkreis.
'can_edit_invoice_prefix' => false,
'exemption_reasons' => TaxExemptionReason::options(),
'pricing_modes' => VatPricingMode::options(),
'saveEndpoint' => '/api/v1/admin/tenant/tax',
@@ -21,6 +21,10 @@ class TenantTaxUpdateController extends CommonController
exemptionReason: TaxExemptionReason::where('slug', $request->input('tax_exemption_reason'))->first(),
exemptionNote: $request->input('tax_exemption_note'),
vatPricingMode: VatPricingMode::tryFrom((string) $request->input('vat_pricing_mode', 'inclusive')) ?? VatPricingMode::Inclusive,
taxNumber: $request->input('tax_number'),
vatId: $request->input('vat_id'),
// invoice_prefix wird hier bewusst nicht durchgereicht -- der Nummernkreis ist nur in der
// Mandanten-Verwaltung änderbar.
));
$response = $action->execute();