Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e726ef34d | ||
|
|
56b603aba0 |
Generated
+127
-1
@@ -1,7 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" packagePrefix="App\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/database/factories" isTestSource="false" packagePrefix="Database\Factories\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/database/seeders" isTestSource="false" packagePrefix="Database\Seeders\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tests\" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/bootstrap/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/storage/app" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/storage/framework" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-dompdf" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/brick/math" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/carbonphp/carbon-doctrine-types" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dflydev/dot-access-data" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/dompdf" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dragonmantank/cron-expression" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/fakerphp/faker" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/filp/whoops" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/fruitcake/php-cors" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/graham-campbell/result-type" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/uri-template" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/hamcrest/hamcrest-php" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/inertiajs/inertia-laravel" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/framework" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/pail" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/pint" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/prompts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/sail" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/serializable-closure" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/commonmark" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/config" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/flysystem" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/flysystem-local" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/mime-type-detection" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/uri" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/league/uri-interfaces" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/mockery/mockery" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nette/schema" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nette/utils" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nunomaduro/collision" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nunomaduro/termwind" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoption/phpoption" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/collection" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/uuid" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/staabm/side-effects-detector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php84" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php85" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php86" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/uid" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/safe" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/voku/portable-ascii" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
||||
Generated
+127
@@ -10,10 +10,137 @@
|
||||
<option name="highlightLevel" value="WARNING" />
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpIncludePathManager">
|
||||
<include_path>
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||
<path value="$PROJECT_DIR$/vendor/hamcrest/hamcrest-php" />
|
||||
<path value="$PROJECT_DIR$/vendor/fakerphp/faker" />
|
||||
<path value="$PROJECT_DIR$/vendor/fruitcake/php-cors" />
|
||||
<path value="$PROJECT_DIR$/vendor/carbonphp/carbon-doctrine-types" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpoption/phpoption" />
|
||||
<path value="$PROJECT_DIR$/vendor/inertiajs/inertia-laravel" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/uri-template" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||
<path value="$PROJECT_DIR$/vendor/nunomaduro/collision" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/nunomaduro/termwind" />
|
||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/dragonmantank/cron-expression" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
|
||||
<path value="$PROJECT_DIR$/vendor/graham-campbell/result-type" />
|
||||
<path value="$PROJECT_DIR$/vendor/barryvdh/laravel-dompdf" />
|
||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||
<path value="$PROJECT_DIR$/vendor/filp/whoops" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/brick/math" />
|
||||
<path value="$PROJECT_DIR$/vendor/voku/portable-ascii" />
|
||||
<path value="$PROJECT_DIR$/vendor/nette/schema" />
|
||||
<path value="$PROJECT_DIR$/vendor/nette/utils" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/mime-type-detection" />
|
||||
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/flysystem-local" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/commonmark" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/uri-interfaces" />
|
||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/uri" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/flysystem" />
|
||||
<path value="$PROJECT_DIR$/vendor/league/config" />
|
||||
<path value="$PROJECT_DIR$/vendor/ramsey/collection" />
|
||||
<path value="$PROJECT_DIR$/vendor/nesbot/carbon" />
|
||||
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||
<path value="$PROJECT_DIR$/vendor/ramsey/uuid" />
|
||||
<path value="$PROJECT_DIR$/vendor/dflydev/dot-access-data" />
|
||||
<path value="$PROJECT_DIR$/vendor/vlucas/phpdotenv" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/pail" />
|
||||
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/pint" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/serializable-closure" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/prompts" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/framework" />
|
||||
<path value="$PROJECT_DIR$/vendor/mockery/mockery" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/sail" />
|
||||
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php86" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.5" />
|
||||
<component name="PhpStanOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpUnit">
|
||||
<phpunit_settings>
|
||||
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
||||
</phpunit_settings>
|
||||
</component>
|
||||
<component name="PsalmOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -30,6 +30,13 @@ const documentTiles = [
|
||||
color: '#e67e22',
|
||||
href: '/teilnahmebescheinigung',
|
||||
},
|
||||
{
|
||||
label: 'Dokument unterschreiben',
|
||||
description: 'Unterschrift auf ein PDF legen und als „eingescanntes" Dokument speichern.',
|
||||
icon: 'signature',
|
||||
color: '#1d4899',
|
||||
href: '/unterschrift',
|
||||
},
|
||||
]
|
||||
|
||||
const externalApps = [
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Rechnung\Actions\GeneratePdf;
|
||||
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
class GeneratePdfCommand
|
||||
{
|
||||
public function __construct(private readonly GeneratePdfRequest $request) {}
|
||||
|
||||
public function execute(): GeneratePdfResponse
|
||||
{
|
||||
$rechnungsnummer = $this->request->rechnungsnummer !== ''
|
||||
? $this->request->rechnungsnummer
|
||||
: date('Y') . '-' . str_pad(random_int(1, 999), 3, '0', STR_PAD_LEFT);
|
||||
|
||||
$positionen = array_values(array_filter(
|
||||
$this->request->positionen,
|
||||
fn($p) => !empty($p['bezeichnung'])
|
||||
));
|
||||
|
||||
$netto = array_sum(array_map(
|
||||
fn($p) => (float) ($p['menge'] ?? 0) * (float) ($p['einzelpreis'] ?? 0),
|
||||
$positionen
|
||||
));
|
||||
$mwst = $netto * $this->request->mwstSatz / 100;
|
||||
$brutto = $netto + $mwst;
|
||||
|
||||
$pdf = Pdf::loadView('pdf.rechnung', [
|
||||
'rechnungsnummer' => $rechnungsnummer,
|
||||
'organisation' => $this->request->organisation,
|
||||
'anrede' => $this->request->anrede,
|
||||
'vorname' => $this->request->vorname,
|
||||
'nachname' => $this->request->nachname,
|
||||
'adresse' => $this->request->adresse,
|
||||
'plz' => $this->request->plz,
|
||||
'ort' => $this->request->ort,
|
||||
'email' => $this->request->email,
|
||||
'iban' => $this->request->iban,
|
||||
'bic' => $this->request->bic,
|
||||
'rechnungsdatum' => $this->request->rechnungsdatum,
|
||||
'zahlungsziel' => $this->request->zahlungsziel,
|
||||
'rechnungsdatumFormatiert' => $this->formatDate($this->request->rechnungsdatum),
|
||||
'zahlungszielFormatiert' => $this->formatDate($this->request->zahlungsziel),
|
||||
'positionen' => $positionen,
|
||||
'mwstSatz' => $this->request->mwstSatz,
|
||||
'netto' => $netto,
|
||||
'mwst' => $mwst,
|
||||
'brutto' => $brutto,
|
||||
'absenderEmail' => $this->request->absenderEmail,
|
||||
'absenderTelefon' => $this->request->absenderTelefon,
|
||||
'kohteDataUri' => $this->imageToDataUri(storage_path('app/elemente/kohte.png')),
|
||||
'logoDataUri' => $this->imageToDataUri(public_path('wbm.png')),
|
||||
]);
|
||||
|
||||
$pdf->setPaper('A4', 'portrait');
|
||||
$pdf->render();
|
||||
|
||||
$pdfContent = $pdf->output();
|
||||
|
||||
return new GeneratePdfResponse(
|
||||
pdfContent: $pdfContent,
|
||||
filename: 'rechnung-' . $rechnungsnummer . '.pdf',
|
||||
);
|
||||
}
|
||||
|
||||
private function formatDate(string $date): string
|
||||
{
|
||||
if (!$date) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$ts = strtotime($date);
|
||||
|
||||
return $ts ? date('d.m.Y', $ts) : $date;
|
||||
}
|
||||
|
||||
|
||||
private function imageToDataUri(string $path): string
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mime = match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'svg' => 'image/svg+xml',
|
||||
default => 'image/png',
|
||||
};
|
||||
|
||||
return 'data:' . $mime . ';base64,' . base64_encode(file_get_contents($path));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Rechnung\Actions\GeneratePdf;
|
||||
|
||||
class GeneratePdfRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $organisation,
|
||||
public readonly string $anrede,
|
||||
public readonly string $vorname,
|
||||
public readonly string $nachname,
|
||||
public readonly string $adresse,
|
||||
public readonly string $plz,
|
||||
public readonly string $ort,
|
||||
public readonly string $email,
|
||||
public readonly string $rechnungsnummer,
|
||||
public readonly string $rechnungsdatum,
|
||||
public readonly string $zahlungsziel,
|
||||
public readonly string $bic,
|
||||
public readonly string $iban,
|
||||
public readonly array $positionen,
|
||||
public readonly int $mwstSatz,
|
||||
public readonly string $absenderEmail,
|
||||
public readonly string $absenderTelefon,
|
||||
) {}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
organisation: $data['organisation'] ?? '',
|
||||
anrede: $data['anrede'] ?? '',
|
||||
vorname: $data['vorname'] ?? '',
|
||||
nachname: $data['nachname'] ?? '',
|
||||
adresse: $data['adresse'] ?? '',
|
||||
plz: $data['plz'] ?? '',
|
||||
ort: $data['ort'] ?? '',
|
||||
email: $data['email'] ?? '',
|
||||
rechnungsnummer: $data['rechnungsnummer'] ?? '',
|
||||
rechnungsdatum: $data['rechnungsdatum'] ?? '',
|
||||
zahlungsziel: $data['zahlungsziel'] ?? '',
|
||||
bic: $data['bic'] ?? '',
|
||||
iban: $data['iban'] ?? '',
|
||||
positionen: $data['positionen'] ?? [],
|
||||
mwstSatz: (int) ($data['mwstSatz'] ?? 19),
|
||||
absenderEmail: $data['absenderEmail'] ?? 'finanzen@sachsen.pfadfinden.de',
|
||||
absenderTelefon: $data['absenderTelefon'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Rechnung\Actions\GeneratePdf;
|
||||
|
||||
class GeneratePdfResponse
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $pdfContent,
|
||||
public readonly string $filename,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Rechnung\Controllers;
|
||||
|
||||
use App\Domains\Rechnung\Actions\GeneratePdf\GeneratePdfCommand;
|
||||
use App\Domains\Rechnung\Actions\GeneratePdf\GeneratePdfRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class GeneratePdfController
|
||||
{
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$pdfRequest = GeneratePdfRequest::fromArray($request->all());
|
||||
$command = new GeneratePdfCommand($pdfRequest);
|
||||
$result = $command->execute();
|
||||
|
||||
return response($result->pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="' . $result->filename . '"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Rechnung\Controllers\GeneratePdfController;
|
||||
use App\Domains\Rechnung\Controllers\RechnungController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/rechnung', RechnungController::class)->name('rechnung');
|
||||
Route::post('/rechnung/pdf', GeneratePdfController::class)->name('rechnung.pdf');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Actions\RenderPreview;
|
||||
|
||||
use Imagick;
|
||||
use ImagickPixel;
|
||||
use RuntimeException;
|
||||
|
||||
class RenderPreviewCommand
|
||||
{
|
||||
/** Aufloesung fuer die Vorschaubilder (klein halten, Platzierung erfolgt relativ). */
|
||||
private const PREVIEW_DPI = 96;
|
||||
|
||||
/** Maximale Seitenzahl, die verarbeitet wird. */
|
||||
public const MAX_PAGES = 30;
|
||||
|
||||
public function __construct(private readonly RenderPreviewRequest $request) {}
|
||||
|
||||
public function execute(): RenderPreviewResponse
|
||||
{
|
||||
$doc = new Imagick();
|
||||
$doc->setResolution(self::PREVIEW_DPI, self::PREVIEW_DPI);
|
||||
|
||||
try {
|
||||
$doc->readImage($this->request->pdfPath);
|
||||
} catch (\ImagickException $e) {
|
||||
throw new RuntimeException('Das PDF konnte nicht gelesen werden.', 0, $e);
|
||||
}
|
||||
|
||||
$pageCount = $doc->getNumberImages();
|
||||
|
||||
if ($pageCount < 1) {
|
||||
throw new RuntimeException('Das PDF enthält keine Seiten.');
|
||||
}
|
||||
|
||||
if ($pageCount > self::MAX_PAGES) {
|
||||
throw new RuntimeException('Das PDF hat zu viele Seiten (max. ' . self::MAX_PAGES . ').');
|
||||
}
|
||||
|
||||
$pages = [];
|
||||
|
||||
for ($i = 0; $i < $pageCount; $i++) {
|
||||
$doc->setIteratorIndex($i);
|
||||
$doc->setImageBackgroundColor(new ImagickPixel('white'));
|
||||
|
||||
if ($doc->getImageAlphaChannel()) {
|
||||
$doc->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
|
||||
}
|
||||
|
||||
$doc->setImageFormat('png');
|
||||
|
||||
$pages[] = [
|
||||
'dataUri' => 'data:image/png;base64,' . base64_encode($doc->getImageBlob()),
|
||||
'width' => $doc->getImageWidth(),
|
||||
'height' => $doc->getImageHeight(),
|
||||
];
|
||||
}
|
||||
|
||||
$doc->clear();
|
||||
|
||||
return new RenderPreviewResponse(pages: $pages, pageCount: $pageCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Actions\RenderPreview;
|
||||
|
||||
class RenderPreviewRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $pdfPath,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Actions\RenderPreview;
|
||||
|
||||
class RenderPreviewResponse
|
||||
{
|
||||
public function __construct(
|
||||
/** @var array<int, array{dataUri: string, width: int, height: int}> */
|
||||
public readonly array $pages,
|
||||
public readonly int $pageCount,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Actions\SignDocument;
|
||||
|
||||
use Imagick;
|
||||
use ImagickPixel;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* imagick-Portierung des falsisign-Ablaufs (sauberer Scan, Modus -c):
|
||||
* PDF -> Seiten rastern -> Unterschrift auf Zielseite compositen -> zurueck zu PDF.
|
||||
* Kein shell_exec; nutzt die imagick-Extension (+ ghostscript-Delegate).
|
||||
*/
|
||||
class SignDocumentCommand
|
||||
{
|
||||
/** Raster-Aufloesung fuer die Ausgabe (analog falsisign density 150). */
|
||||
private const OUTPUT_DPI = 150;
|
||||
|
||||
/** Maximale Seitenzahl, die verarbeitet wird. */
|
||||
public const MAX_PAGES = 30;
|
||||
|
||||
public function __construct(private readonly SignDocumentRequest $request) {}
|
||||
|
||||
public function execute(): SignDocumentResponse
|
||||
{
|
||||
$signature = $this->prepareOverlay($this->request->signaturPath, 'Unterschrift-Bild');
|
||||
|
||||
$stamp = $this->request->stempelPath !== null
|
||||
? $this->prepareOverlay($this->request->stempelPath, 'Stempel-Bild')
|
||||
: null;
|
||||
|
||||
$doc = new Imagick();
|
||||
$doc->setResolution(self::OUTPUT_DPI, self::OUTPUT_DPI);
|
||||
|
||||
try {
|
||||
$doc->readImage($this->request->pdfPath);
|
||||
} catch (\ImagickException $e) {
|
||||
$signature->clear();
|
||||
$stamp?->clear();
|
||||
throw new RuntimeException('Das PDF konnte nicht gelesen werden.', 0, $e);
|
||||
}
|
||||
|
||||
$pageCount = $doc->getNumberImages();
|
||||
|
||||
if ($pageCount < 1) {
|
||||
throw new RuntimeException('Das PDF enthält keine Seiten.');
|
||||
}
|
||||
|
||||
if ($pageCount > self::MAX_PAGES) {
|
||||
throw new RuntimeException('Das PDF hat zu viele Seiten (max. ' . self::MAX_PAGES . ').');
|
||||
}
|
||||
|
||||
$targetIndex = min($this->request->page, $pageCount) - 1;
|
||||
|
||||
for ($i = 0; $i < $pageCount; $i++) {
|
||||
$doc->setIteratorIndex($i);
|
||||
|
||||
// Transparenten PDF-Hintergrund gegen Weiss aufloesen (sonst schwarz).
|
||||
$doc->setImageBackgroundColor(new ImagickPixel('white'));
|
||||
if ($doc->getImageAlphaChannel()) {
|
||||
$doc->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
|
||||
}
|
||||
|
||||
if ($i === $targetIndex) {
|
||||
// Stempel zuerst, damit die Unterschrift ggf. darueber liegt.
|
||||
if ($stamp !== null) {
|
||||
$this->composeOverlay(
|
||||
$doc,
|
||||
$stamp,
|
||||
$this->request->stempelXFrac,
|
||||
$this->request->stempelYFrac,
|
||||
$this->request->stempelWidthFrac
|
||||
);
|
||||
}
|
||||
|
||||
$this->composeOverlay(
|
||||
$doc,
|
||||
$signature,
|
||||
$this->request->xFrac,
|
||||
$this->request->yFrac,
|
||||
$this->request->widthFrac
|
||||
);
|
||||
}
|
||||
|
||||
// Sauberer Scan-Look: Graustufen, aber kein Rauschen/keine Rotation.
|
||||
if ($this->request->graustufen) {
|
||||
$doc->transformImageColorspace(Imagick::COLORSPACE_GRAY);
|
||||
}
|
||||
|
||||
// Format pro Seite setzen, damit getImagesBlob() ein mehrseitiges PDF liefert.
|
||||
$doc->setImageFormat('pdf');
|
||||
}
|
||||
|
||||
$signature->clear();
|
||||
$stamp?->clear();
|
||||
|
||||
$doc->resetIterator();
|
||||
$pdfContent = $doc->getImagesBlob();
|
||||
$doc->clear();
|
||||
|
||||
return new SignDocumentResponse(
|
||||
pdfContent: $pdfContent,
|
||||
filename: $this->request->filename,
|
||||
);
|
||||
}
|
||||
|
||||
/** Overlay (Unterschrift/Stempel) laden, Weiss transparent machen und auf den Inhalt zuschneiden. */
|
||||
private function prepareOverlay(string $path, string $label): Imagick
|
||||
{
|
||||
$overlay = new Imagick();
|
||||
|
||||
try {
|
||||
$overlay->readImage($path);
|
||||
} catch (\ImagickException $e) {
|
||||
throw new RuntimeException('Das ' . $label . ' konnte nicht gelesen werden.', 0, $e);
|
||||
}
|
||||
|
||||
$overlay->setImageFormat('png');
|
||||
$overlay->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
|
||||
|
||||
$range = $overlay->getQuantumRange()['quantumRangeLong'];
|
||||
|
||||
// Weiss (inkl. leicht abweichender Scan-/Foto-Toene) transparent setzen.
|
||||
$overlay->transparentPaintImage(new ImagickPixel('white'), 0.0, 0.15 * $range, false);
|
||||
|
||||
// Ueberfluessigen Rand entfernen, damit die Positionierung exakt ist.
|
||||
$overlay->trimImage(0.1 * $range);
|
||||
$overlay->setImagePage(0, 0, 0, 0);
|
||||
|
||||
return $overlay;
|
||||
}
|
||||
|
||||
/** Overlay skaliert an der gewuenschten Position auf die aktuelle Seite legen. */
|
||||
private function composeOverlay(Imagick $doc, Imagick $overlay, float $xFrac, float $yFrac, float $widthFrac): void
|
||||
{
|
||||
$pageWidth = $doc->getImageWidth();
|
||||
$pageHeight = $doc->getImageHeight();
|
||||
|
||||
$targetWidth = max(1, (int) round($widthFrac * $pageWidth));
|
||||
|
||||
$placed = clone $overlay;
|
||||
$placed->resizeImage($targetWidth, 0, Imagick::FILTER_LANCZOS, 1);
|
||||
|
||||
$x = (int) round($xFrac * $pageWidth);
|
||||
$y = (int) round($yFrac * $pageHeight);
|
||||
|
||||
$doc->compositeImage($placed, Imagick::COMPOSITE_OVER, $x, $y);
|
||||
$placed->clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Actions\SignDocument;
|
||||
|
||||
class SignDocumentRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $pdfPath,
|
||||
public readonly string $signaturPath,
|
||||
public readonly int $page,
|
||||
public readonly float $xFrac,
|
||||
public readonly float $yFrac,
|
||||
public readonly float $widthFrac,
|
||||
public readonly bool $graustufen,
|
||||
public readonly string $filename,
|
||||
public readonly ?string $stempelPath = null,
|
||||
public readonly float $stempelXFrac = 0.0,
|
||||
public readonly float $stempelYFrac = 0.0,
|
||||
public readonly float $stempelWidthFrac = 0.18,
|
||||
) {}
|
||||
|
||||
public static function fromArray(array $data, string $pdfPath, string $signaturPath, ?string $stempelPath = null): self
|
||||
{
|
||||
return new self(
|
||||
pdfPath: $pdfPath,
|
||||
signaturPath: $signaturPath,
|
||||
page: max(1, (int) ($data['page'] ?? 1)),
|
||||
xFrac: self::clamp((float) ($data['xFrac'] ?? 0)),
|
||||
yFrac: self::clamp((float) ($data['yFrac'] ?? 0)),
|
||||
widthFrac: self::clamp((float) ($data['widthFrac'] ?? 0.2), 0.02, 1.0),
|
||||
graustufen: filter_var($data['graustufen'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
||||
filename: 'signiert.pdf',
|
||||
stempelPath: $stempelPath,
|
||||
stempelXFrac: self::clamp((float) ($data['stempelXFrac'] ?? 0)),
|
||||
stempelYFrac: self::clamp((float) ($data['stempelYFrac'] ?? 0)),
|
||||
stempelWidthFrac: self::clamp((float) ($data['stempelWidthFrac'] ?? 0.18), 0.02, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
private static function clamp(float $value, float $min = 0.0, float $max = 1.0): float
|
||||
{
|
||||
return max($min, min($max, $value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Actions\SignDocument;
|
||||
|
||||
class SignDocumentResponse
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $pdfContent,
|
||||
public readonly string $filename,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Controllers;
|
||||
|
||||
use App\Domains\Unterschrift\Actions\RenderPreview\RenderPreviewCommand;
|
||||
use App\Domains\Unterschrift\Actions\RenderPreview\RenderPreviewRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
class PreviewPageController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'pdf' => 'required|file|mimetypes:application/pdf|max:20480',
|
||||
]);
|
||||
|
||||
try {
|
||||
$command = new RenderPreviewCommand(
|
||||
new RenderPreviewRequest($request->file('pdf')->getRealPath())
|
||||
);
|
||||
$response = $command->execute();
|
||||
} catch (RuntimeException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'pages' => $response->pages,
|
||||
'pageCount' => $response->pageCount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Controllers;
|
||||
|
||||
use App\Domains\Unterschrift\Actions\SignDocument\SignDocumentCommand;
|
||||
use App\Domains\Unterschrift\Actions\SignDocument\SignDocumentRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use RuntimeException;
|
||||
|
||||
class SignController
|
||||
{
|
||||
public function __invoke(Request $request): Response|JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'pdf' => 'required|file|mimetypes:application/pdf|max:20480',
|
||||
'signatur' => 'required|file|mimetypes:image/png,image/jpeg,image/webp|max:8192',
|
||||
'page' => 'required|integer|min:1',
|
||||
'xFrac' => 'required|numeric|between:0,1',
|
||||
'yFrac' => 'required|numeric|between:0,1',
|
||||
'widthFrac' => 'required|numeric|between:0,1',
|
||||
'stempel' => 'nullable|file|mimetypes:image/png,image/jpeg,image/webp|max:8192',
|
||||
'stempelXFrac' => 'nullable|numeric|between:0,1',
|
||||
'stempelYFrac' => 'nullable|numeric|between:0,1',
|
||||
'stempelWidthFrac' => 'nullable|numeric|between:0,1',
|
||||
]);
|
||||
|
||||
try {
|
||||
$signRequest = SignDocumentRequest::fromArray(
|
||||
$request->all(),
|
||||
$request->file('pdf')->getRealPath(),
|
||||
$request->file('signatur')->getRealPath(),
|
||||
$request->file('stempel')?->getRealPath(),
|
||||
);
|
||||
|
||||
$result = (new SignDocumentCommand($signRequest))->execute();
|
||||
} catch (RuntimeException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response($result->pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="' . $result->filename . '"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Unterschrift\Controllers;
|
||||
|
||||
use App\Providers\InertiaProvider;
|
||||
use Inertia\Response;
|
||||
|
||||
class UnterschriftController
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
return (new InertiaProvider('Unterschrift/Unterschrift', []))->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Unterschrift\Controllers\PreviewPageController;
|
||||
use App\Domains\Unterschrift\Controllers\SignController;
|
||||
use App\Domains\Unterschrift\Controllers\UnterschriftController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/unterschrift', UnterschriftController::class)->name('unterschrift');
|
||||
Route::post('/unterschrift/preview', PreviewPageController::class)->name('unterschrift.preview');
|
||||
Route::post('/unterschrift/sign', SignController::class)->name('unterschrift.sign');
|
||||
@@ -0,0 +1,565 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/layouts/AppLayout.vue'
|
||||
import { ref, computed, getCurrentInstance } from 'vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const step = ref(1) // 1 = Upload, 2 = Platzieren
|
||||
const previewLoading = ref(false)
|
||||
const signLoading = ref(false)
|
||||
|
||||
const pdfFile = ref(null)
|
||||
const signaturFile = ref(null)
|
||||
const signaturUrl = ref('')
|
||||
const stempelFile = ref(null) // optional
|
||||
const stempelUrl = ref('')
|
||||
|
||||
const dragPdf = ref(false)
|
||||
const dragSig = ref(false)
|
||||
const dragStempel = ref(false)
|
||||
|
||||
const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp']
|
||||
|
||||
const pages = ref([]) // [{ dataUri, width, height }]
|
||||
const selectedPage = ref(1) // 1-basiert
|
||||
const graustufen = ref(true)
|
||||
|
||||
// Platzierung relativ zur Seite (0..1)
|
||||
const placement = ref({ xFrac: 0.55, yFrac: 0.8, widthFrac: 0.25 })
|
||||
const stempelPlacement = ref({ xFrac: 0.15, yFrac: 0.78, widthFrac: 0.18 })
|
||||
|
||||
const stageRef = ref(null)
|
||||
|
||||
const currentPage = computed(() => pages.value[selectedPage.value - 1] ?? null)
|
||||
const canPreview = computed(() => pdfFile.value && signaturFile.value)
|
||||
|
||||
function csrf() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content ?? ''
|
||||
}
|
||||
|
||||
function setPdf(file) {
|
||||
if (!file) return
|
||||
if (file.type !== 'application/pdf') {
|
||||
proxy.$toast.error('Bitte eine PDF-Datei auswählen.')
|
||||
return
|
||||
}
|
||||
pdfFile.value = file
|
||||
}
|
||||
|
||||
function setSignatur(file) {
|
||||
if (!file) return
|
||||
if (!IMAGE_TYPES.includes(file.type)) {
|
||||
proxy.$toast.error('Bitte ein Bild (PNG, JPG oder WebP) auswählen.')
|
||||
return
|
||||
}
|
||||
signaturFile.value = file
|
||||
if (signaturUrl.value) URL.revokeObjectURL(signaturUrl.value)
|
||||
signaturUrl.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function setStempel(file) {
|
||||
if (!file) return
|
||||
if (!IMAGE_TYPES.includes(file.type)) {
|
||||
proxy.$toast.error('Bitte ein Bild (PNG, JPG oder WebP) auswählen.')
|
||||
return
|
||||
}
|
||||
stempelFile.value = file
|
||||
if (stempelUrl.value) URL.revokeObjectURL(stempelUrl.value)
|
||||
stempelUrl.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function removeStempel() {
|
||||
stempelFile.value = null
|
||||
if (stempelUrl.value) URL.revokeObjectURL(stempelUrl.value)
|
||||
stempelUrl.value = ''
|
||||
}
|
||||
|
||||
function onPdfChange(e) {
|
||||
setPdf(e.target.files[0] ?? null)
|
||||
}
|
||||
|
||||
function onSignaturChange(e) {
|
||||
setSignatur(e.target.files[0] ?? null)
|
||||
}
|
||||
|
||||
function onStempelChange(e) {
|
||||
setStempel(e.target.files[0] ?? null)
|
||||
}
|
||||
|
||||
function onDropPdf(e) {
|
||||
dragPdf.value = false
|
||||
setPdf(e.dataTransfer.files[0] ?? null)
|
||||
}
|
||||
|
||||
function onDropSig(e) {
|
||||
dragSig.value = false
|
||||
setSignatur(e.dataTransfer.files[0] ?? null)
|
||||
}
|
||||
|
||||
function onDropStempel(e) {
|
||||
dragStempel.value = false
|
||||
setStempel(e.dataTransfer.files[0] ?? null)
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
if (!canPreview.value) return
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const data = new FormData()
|
||||
data.append('pdf', pdfFile.value)
|
||||
const res = await fetch('/unterschrift/preview', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': csrf() },
|
||||
body: data,
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status === 413) {
|
||||
proxy.$toast.error('Die PDF-Datei ist zu groß.')
|
||||
return
|
||||
}
|
||||
const err = await res.json().catch(() => ({}))
|
||||
proxy.$toast.error(err.message ?? 'Die Vorschau konnte nicht erstellt werden.')
|
||||
return
|
||||
}
|
||||
const json = await res.json()
|
||||
pages.value = json.pages
|
||||
selectedPage.value = 1
|
||||
step.value = 2
|
||||
} catch {
|
||||
proxy.$toast.error('Netzwerkfehler beim Erstellen der Vorschau.')
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drag von Unterschrift/Stempel ──
|
||||
let dragState = null
|
||||
|
||||
function placementFor(which) {
|
||||
return which === 'stempel' ? stempelPlacement : placement
|
||||
}
|
||||
|
||||
function startDrag(e, which) {
|
||||
const rect = stageRef.value.getBoundingClientRect()
|
||||
const p = placementFor(which).value
|
||||
dragState = {
|
||||
which,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
origX: p.xFrac,
|
||||
origY: p.yFrac,
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
}
|
||||
window.addEventListener('pointermove', onDrag)
|
||||
window.addEventListener('pointerup', endDrag)
|
||||
}
|
||||
|
||||
function onDrag(e) {
|
||||
if (!dragState) return
|
||||
const p = placementFor(dragState.which).value
|
||||
const dx = (e.clientX - dragState.startX) / dragState.w
|
||||
const dy = (e.clientY - dragState.startY) / dragState.h
|
||||
p.xFrac = clamp(dragState.origX + dx, 0, 1 - p.widthFrac * 0.2)
|
||||
p.yFrac = clamp(dragState.origY + dy, 0, 0.99)
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
dragState = null
|
||||
window.removeEventListener('pointermove', onDrag)
|
||||
window.removeEventListener('pointerup', endDrag)
|
||||
}
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, v))
|
||||
}
|
||||
|
||||
async function submitSign() {
|
||||
signLoading.value = true
|
||||
try {
|
||||
const data = new FormData()
|
||||
data.append('pdf', pdfFile.value)
|
||||
data.append('signatur', signaturFile.value)
|
||||
data.append('page', selectedPage.value)
|
||||
data.append('xFrac', placement.value.xFrac)
|
||||
data.append('yFrac', placement.value.yFrac)
|
||||
data.append('widthFrac', placement.value.widthFrac)
|
||||
data.append('graustufen', graustufen.value ? '1' : '0')
|
||||
|
||||
if (stempelFile.value) {
|
||||
data.append('stempel', stempelFile.value)
|
||||
data.append('stempelXFrac', stempelPlacement.value.xFrac)
|
||||
data.append('stempelYFrac', stempelPlacement.value.yFrac)
|
||||
data.append('stempelWidthFrac', stempelPlacement.value.widthFrac)
|
||||
}
|
||||
|
||||
const res = await fetch('/unterschrift/sign', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': csrf() },
|
||||
body: data,
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status === 413) {
|
||||
proxy.$toast.error('Die hochgeladenen Dateien sind zu groß.')
|
||||
return
|
||||
}
|
||||
const err = await res.json().catch(() => ({}))
|
||||
proxy.$toast.error(err.message ?? 'Das Dokument konnte nicht signiert werden.')
|
||||
return
|
||||
}
|
||||
const blob = await res.blob()
|
||||
window.open(URL.createObjectURL(blob), '_blank')
|
||||
} catch {
|
||||
proxy.$toast.error('Netzwerkfehler beim Signieren.')
|
||||
} finally {
|
||||
signLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
step.value = 1
|
||||
pages.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Dokument unterschreiben">
|
||||
<div class="page">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<div class="page-header">
|
||||
<nav class="breadcrumb">
|
||||
<a href="/" class="bc-link">Startseite</a>
|
||||
<font-awesome-icon icon="chevron-right" class="bc-sep" />
|
||||
<span class="bc-current">Dokument unterschreiben</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Titel -->
|
||||
<div class="page-title">
|
||||
<div class="title-icon">
|
||||
<font-awesome-icon icon="signature" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="title-text">Dokument unterschreiben</h1>
|
||||
<p class="title-sub">
|
||||
PDF und Unterschrift hochladen, platzieren und als „eingescanntes" Dokument herunterladen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SCHRITT 1: Upload ── -->
|
||||
<div v-if="step === 1" class="card upload-card">
|
||||
<h2 class="card-heading">Dateien auswählen</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">PDF-Dokument</label>
|
||||
<label
|
||||
class="dropzone"
|
||||
:class="{ 'dropzone--active': dragPdf, 'dropzone--filled': pdfFile }"
|
||||
@dragover.prevent="dragPdf = true"
|
||||
@dragenter.prevent="dragPdf = true"
|
||||
@dragleave.prevent="dragPdf = false"
|
||||
@drop.prevent="onDropPdf"
|
||||
>
|
||||
<input type="file" accept="application/pdf" class="dropzone-input" @change="onPdfChange" />
|
||||
<font-awesome-icon icon="file-pdf" class="dropzone-icon" />
|
||||
<span v-if="pdfFile" class="dropzone-file">{{ pdfFile.name }}</span>
|
||||
<span v-else class="dropzone-hint">
|
||||
PDF hierher ziehen oder <span class="dropzone-link">durchsuchen</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Unterschrift (Foto oder Scan, PNG/JPG)</label>
|
||||
<label
|
||||
class="dropzone"
|
||||
:class="{ 'dropzone--active': dragSig, 'dropzone--filled': signaturFile }"
|
||||
@dragover.prevent="dragSig = true"
|
||||
@dragenter.prevent="dragSig = true"
|
||||
@dragleave.prevent="dragSig = false"
|
||||
@drop.prevent="onDropSig"
|
||||
>
|
||||
<input type="file" accept="image/png,image/jpeg,image/webp" class="dropzone-input" @change="onSignaturChange" />
|
||||
<template v-if="signaturUrl">
|
||||
<div class="sig-preview">
|
||||
<img :src="signaturUrl" alt="Unterschrift" />
|
||||
</div>
|
||||
<span class="dropzone-file">{{ signaturFile.name }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<font-awesome-icon icon="signature" class="dropzone-icon" />
|
||||
<span class="dropzone-hint">
|
||||
Bild hierher ziehen oder <span class="dropzone-link">durchsuchen</span>
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Stempel <span class="optional">(optional)</span>
|
||||
</label>
|
||||
<label
|
||||
class="dropzone"
|
||||
:class="{ 'dropzone--active': dragStempel, 'dropzone--filled': stempelFile }"
|
||||
@dragover.prevent="dragStempel = true"
|
||||
@dragenter.prevent="dragStempel = true"
|
||||
@dragleave.prevent="dragStempel = false"
|
||||
@drop.prevent="onDropStempel"
|
||||
>
|
||||
<input type="file" accept="image/png,image/jpeg,image/webp" class="dropzone-input" @change="onStempelChange" />
|
||||
<template v-if="stempelUrl">
|
||||
<div class="sig-preview">
|
||||
<img :src="stempelUrl" alt="Stempel" />
|
||||
</div>
|
||||
<span class="dropzone-file">{{ stempelFile.name }}</span>
|
||||
<button type="button" class="dropzone-remove" @click.prevent.stop="removeStempel">
|
||||
<font-awesome-icon icon="trash" /> Entfernen
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<font-awesome-icon icon="stamp" class="dropzone-icon" />
|
||||
<span class="dropzone-hint">
|
||||
Stempel hierher ziehen oder <span class="dropzone-link">durchsuchen</span>
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" :disabled="!canPreview || previewLoading" @click="loadPreview">
|
||||
<font-awesome-icon :icon="previewLoading ? 'file-invoice' : 'chevron-right'" />
|
||||
{{ previewLoading ? 'Vorschau wird erstellt…' : 'Weiter: Platzieren' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── SCHRITT 2: Platzieren ── -->
|
||||
<div v-else class="sign-grid">
|
||||
|
||||
<!-- Steuerung -->
|
||||
<div class="card">
|
||||
<h2 class="card-heading">Platzierung</h2>
|
||||
|
||||
<div class="field" v-if="pages.length > 1">
|
||||
<label class="label">Seite</label>
|
||||
<div class="page-badges">
|
||||
<button
|
||||
v-for="n in pages.length"
|
||||
:key="n"
|
||||
class="page-badge"
|
||||
:class="{ 'page-badge--active': selectedPage === n }"
|
||||
@click="selectedPage = n"
|
||||
>{{ n }}</button>
|
||||
</div>
|
||||
<p class="hint">Die Unterschrift wird auf Seite {{ selectedPage }} eingefügt.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Größe der Unterschrift</label>
|
||||
<input
|
||||
type="range" min="0.05" max="0.6" step="0.01"
|
||||
v-model.number="placement.widthFrac" class="range"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="stempelFile">
|
||||
<label class="label">Größe des Stempels</label>
|
||||
<input
|
||||
type="range" min="0.05" max="0.6" step="0.01"
|
||||
v-model.number="stempelPlacement.widthFrac" class="range"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="check-row">
|
||||
<input type="checkbox" v-model="graustufen" />
|
||||
<span>Graustufen (Scan-Look)</span>
|
||||
</label>
|
||||
|
||||
<p class="hint">
|
||||
Ziehe die Unterschrift<template v-if="stempelFile"> und den Stempel</template>
|
||||
im Vorschaubild an die gewünschte Stelle.
|
||||
</p>
|
||||
|
||||
<div class="btn-col">
|
||||
<button class="btn-primary" :disabled="signLoading" @click="submitSign">
|
||||
<font-awesome-icon :icon="signLoading ? 'file-invoice' : 'download'" />
|
||||
{{ signLoading ? 'Wird erstellt…' : 'Signieren & herunterladen' }}
|
||||
</button>
|
||||
<button class="btn-back" @click="reset">
|
||||
<font-awesome-icon icon="chevron-left" />
|
||||
Zurück
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vorschau mit Overlay -->
|
||||
<div class="card preview-card">
|
||||
<div class="stage-wrap">
|
||||
<div ref="stageRef" class="stage" v-if="currentPage">
|
||||
<img class="stage-page" :src="currentPage.dataUri" alt="Seitenvorschau" draggable="false" />
|
||||
<img
|
||||
v-if="stempelUrl"
|
||||
class="stage-overlay stage-stempel"
|
||||
:src="stempelUrl"
|
||||
alt="Stempel"
|
||||
draggable="false"
|
||||
:style="{
|
||||
left: (stempelPlacement.xFrac * 100) + '%',
|
||||
top: (stempelPlacement.yFrac * 100) + '%',
|
||||
width: (stempelPlacement.widthFrac * 100) + '%',
|
||||
}"
|
||||
@pointerdown.prevent="startDrag($event, 'stempel')"
|
||||
/>
|
||||
<img
|
||||
v-if="signaturUrl"
|
||||
class="stage-overlay stage-sig"
|
||||
:src="signaturUrl"
|
||||
alt="Unterschrift"
|
||||
draggable="false"
|
||||
:style="{
|
||||
left: (placement.xFrac * 100) + '%',
|
||||
top: (placement.yFrac * 100) + '%',
|
||||
width: (placement.widthFrac * 100) + '%',
|
||||
}"
|
||||
@pointerdown.prevent="startDrag($event, 'sig')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page { display: flex; flex-direction: column; gap: 20px; }
|
||||
|
||||
/* Breadcrumb */
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: #6b7280; }
|
||||
.bc-link { color: #1d4899; text-decoration: none; }
|
||||
.bc-link:hover { text-decoration: underline; }
|
||||
.bc-sep { font-size: 0.7rem; color: #9ca3af; }
|
||||
.bc-current { color: #374151; font-weight: 500; }
|
||||
|
||||
/* Titel */
|
||||
.page-title { display: flex; align-items: center; gap: 14px; }
|
||||
.title-icon {
|
||||
width: 44px; height: 44px; background: #eef2ff; color: #1d4899;
|
||||
border-radius: 10px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.2rem; flex-shrink: 0;
|
||||
}
|
||||
.title-text { margin: 0 0 3px; font-size: 1.25rem; font-weight: 700; color: #111827; }
|
||||
.title-sub { margin: 0; font-size: 0.85rem; color: #6b7280; }
|
||||
|
||||
/* Karte */
|
||||
.card {
|
||||
background: white; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.07);
|
||||
padding: 24px; border: 1px solid #f0f0f0;
|
||||
}
|
||||
.card-heading { margin: 0 0 18px; font-size: 1rem; font-weight: 700; color: #111827; }
|
||||
|
||||
.upload-card { max-width: 560px; display: flex; flex-direction: column; gap: 18px; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.label { font-size: 0.8rem; font-weight: 600; color: #374151; }
|
||||
|
||||
.dropzone {
|
||||
position: relative;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 8px; text-align: center;
|
||||
min-height: 120px; padding: 18px;
|
||||
border: 2px dashed #d1d5db; border-radius: 10px;
|
||||
background: #fafafa; cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.dropzone:hover { border-color: #1d4899; background: #f5f7fd; }
|
||||
.dropzone--active { border-color: #1d4899; background: #eef2ff; }
|
||||
.dropzone--filled { border-style: solid; border-color: #c7d2fe; background: #fff; }
|
||||
|
||||
.dropzone-input {
|
||||
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||
opacity: 0; cursor: pointer;
|
||||
}
|
||||
.dropzone-icon { font-size: 1.6rem; color: #9ca3af; }
|
||||
.dropzone--filled .dropzone-icon { color: #1d4899; }
|
||||
.dropzone-hint { font-size: 0.82rem; color: #6b7280; }
|
||||
.dropzone-link { color: #1d4899; font-weight: 600; text-decoration: underline; }
|
||||
.dropzone-file { font-size: 0.8rem; font-weight: 600; color: #374151; word-break: break-all; }
|
||||
.dropzone-remove {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
background: transparent; border: none; cursor: pointer;
|
||||
font-size: 0.76rem; color: #ef4444; padding: 2px 4px;
|
||||
}
|
||||
.dropzone-remove:hover { text-decoration: underline; }
|
||||
|
||||
.optional { font-weight: 500; color: #9ca3af; }
|
||||
|
||||
.sig-preview {
|
||||
padding: 10px; border: 1px solid #f0f0f0; border-radius: 8px;
|
||||
background: repeating-conic-gradient(#f3f4f6 0% 25%, #fff 0% 50%) 0 / 16px 16px;
|
||||
max-width: 240px;
|
||||
}
|
||||
.sig-preview img { max-width: 100%; max-height: 90px; display: block; }
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
display: flex; align-items: center; justify-content: center; gap: 9px;
|
||||
padding: 11px 16px; background: #1d4899; color: white; border: none;
|
||||
border-radius: 9px; font-size: 0.92rem; font-weight: 700; cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) { background: #163a7a; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
.btn-back {
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
background: white; color: #374151; border: 1px solid #e5e7eb; border-radius: 8px;
|
||||
padding: 10px 16px; font-size: 0.88rem; font-weight: 600; cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.btn-back:hover { background: #f9fafb; border-color: #d1d5db; }
|
||||
|
||||
.btn-col { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; }
|
||||
|
||||
/* Schritt 2 Layout */
|
||||
.sign-grid { display: grid; grid-template-columns: 320px 1fr; gap: 20px; align-items: start; }
|
||||
|
||||
.page-badges { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.page-badge {
|
||||
min-width: 34px; padding: 5px 10px; border: 1px solid #e5e7eb; border-radius: 7px;
|
||||
background: white; font-size: 0.82rem; font-weight: 600; color: #6b7280; cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.page-badge:hover { border-color: #1d4899; color: #1d4899; }
|
||||
.page-badge--active { background: #1d4899; border-color: #1d4899; color: white; }
|
||||
|
||||
.range { width: 100%; accent-color: #1d4899; }
|
||||
|
||||
.check-row { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: #374151; cursor: pointer; margin-top: 4px; }
|
||||
.check-row input { accent-color: #1d4899; }
|
||||
|
||||
.hint { font-size: 0.78rem; color: #9ca3af; margin: 10px 0 0; line-height: 1.5; }
|
||||
|
||||
/* Vorschau-Bühne */
|
||||
.preview-card { padding: 16px; background: #f5f6fa; }
|
||||
.stage-wrap { display: flex; justify-content: center; }
|
||||
.stage {
|
||||
position: relative; display: inline-block; max-width: 100%;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.12); background: white;
|
||||
}
|
||||
.stage-page { display: block; max-width: 100%; height: auto; user-select: none; }
|
||||
.stage-overlay {
|
||||
position: absolute; cursor: grab; user-select: none;
|
||||
mix-blend-mode: multiply; touch-action: none;
|
||||
}
|
||||
.stage-overlay:active { cursor: grabbing; }
|
||||
.stage-stempel { outline: 1px dashed rgba(29, 72, 153, 0.35); outline-offset: 2px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sign-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.5",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/framework": "^13.0"
|
||||
},
|
||||
|
||||
Generated
+523
-1
@@ -4,8 +4,85 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "844b0bde1d19fc4ee7aed07fa973aaff",
|
||||
"content-hash": "e9179c934253bee1b1afef146ded5a2b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
"version": "v3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-dompdf.git",
|
||||
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
|
||||
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dompdf/dompdf": "^3.0",
|
||||
"illuminate/support": "^9|^10|^11|^12|^13.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^2.7|^3.0",
|
||||
"orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
|
||||
"phpro/grumphp": "^2.5",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
|
||||
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
|
||||
},
|
||||
"providers": [
|
||||
"Barryvdh\\DomPDF\\ServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\DomPDF\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A DOMPDF Wrapper for Laravel",
|
||||
"keywords": [
|
||||
"dompdf",
|
||||
"laravel",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
|
||||
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://fruitcake.nl",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/barryvdh",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-21T08:51:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.18.0",
|
||||
@@ -376,6 +453,161 @@
|
||||
],
|
||||
"time": "2024-02-05T11:56:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"version": "v3.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/dompdf.git",
|
||||
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
|
||||
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dompdf/php-font-lib": "^1.0.0",
|
||||
"dompdf/php-svg-lib": "^1.0.0",
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"masterminds/html5": "^2.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"mockery/mockery": "^1.3",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||
"source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
|
||||
},
|
||||
"time": "2026-03-03T13:54:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/php-font-lib",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FontLib\\": "src/FontLib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The FontLib Community",
|
||||
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||
"homepage": "https://github.com/dompdf/php-font-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
|
||||
},
|
||||
"time": "2026-01-20T14:10:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/php-svg-lib",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabberworm/php-css-parser": "^8.4 || ^9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Svg\\": "src/Svg"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The SvgLib Community",
|
||||
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse and export to PDF SVG files.",
|
||||
"homepage": "https://github.com/dompdf/php-svg-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
|
||||
},
|
||||
"time": "2026-01-02T16:01:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dragonmantank/cron-expression",
|
||||
"version": "v3.6.0",
|
||||
@@ -2033,6 +2265,73 @@
|
||||
],
|
||||
"time": "2026-03-08T20:05:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fd5018f6815fff903946d0564977b44ce8010e29"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29",
|
||||
"reference": "fd5018f6815fff903946d0564977b44ce8010e29",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Masterminds\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
"dom",
|
||||
"html",
|
||||
"parser",
|
||||
"querypath",
|
||||
"serializer",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.1"
|
||||
},
|
||||
"time": "2026-06-23T18:43:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.10.0",
|
||||
@@ -3171,6 +3470,86 @@
|
||||
},
|
||||
"time": "2026-06-18T03:57:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v9.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||
"reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f",
|
||||
"reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
||||
"phpstan/extension-installer": "1.4.3",
|
||||
"phpstan/phpstan": "1.12.33 || 2.2.2",
|
||||
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11",
|
||||
"phpunit/phpunit": "8.5.52",
|
||||
"rawr/phpunit-data-provider": "3.3.1",
|
||||
"rector/rector": "1.2.10 || 2.4.6",
|
||||
"rector/type-perfect": "1.0.0 || 2.1.3",
|
||||
"squizlabs/php_codesniffer": "4.0.1",
|
||||
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Rule/Rule.php",
|
||||
"src/RuleSet/RuleContainer.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Raphael Schweikert"
|
||||
},
|
||||
{
|
||||
"name": "Oliver Klee",
|
||||
"email": "github@oliverklee.de"
|
||||
},
|
||||
{
|
||||
"name": "Jake Hotson",
|
||||
"email": "jake.github@qzdesign.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Parser for CSS Files written in PHP",
|
||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||
"keywords": [
|
||||
"css",
|
||||
"parser",
|
||||
"stylesheet"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0"
|
||||
},
|
||||
"time": "2026-06-18T15:10:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v8.1.0",
|
||||
@@ -5650,6 +6029,149 @@
|
||||
],
|
||||
"time": "2026-06-09T10:54:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "thecodingmachine/safe",
|
||||
"version": "v3.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thecodingmachine/safe.git",
|
||||
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
|
||||
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||
"phpstan/phpstan": "^2",
|
||||
"phpunit/phpunit": "^10",
|
||||
"squizlabs/php_codesniffer": "^3.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/special_cases.php",
|
||||
"generated/apache.php",
|
||||
"generated/apcu.php",
|
||||
"generated/array.php",
|
||||
"generated/bzip2.php",
|
||||
"generated/calendar.php",
|
||||
"generated/classobj.php",
|
||||
"generated/com.php",
|
||||
"generated/cubrid.php",
|
||||
"generated/curl.php",
|
||||
"generated/datetime.php",
|
||||
"generated/dir.php",
|
||||
"generated/eio.php",
|
||||
"generated/errorfunc.php",
|
||||
"generated/exec.php",
|
||||
"generated/fileinfo.php",
|
||||
"generated/filesystem.php",
|
||||
"generated/filter.php",
|
||||
"generated/fpm.php",
|
||||
"generated/ftp.php",
|
||||
"generated/funchand.php",
|
||||
"generated/gettext.php",
|
||||
"generated/gmp.php",
|
||||
"generated/gnupg.php",
|
||||
"generated/hash.php",
|
||||
"generated/ibase.php",
|
||||
"generated/ibmDb2.php",
|
||||
"generated/iconv.php",
|
||||
"generated/image.php",
|
||||
"generated/imap.php",
|
||||
"generated/info.php",
|
||||
"generated/inotify.php",
|
||||
"generated/json.php",
|
||||
"generated/ldap.php",
|
||||
"generated/libxml.php",
|
||||
"generated/lzf.php",
|
||||
"generated/mailparse.php",
|
||||
"generated/mbstring.php",
|
||||
"generated/misc.php",
|
||||
"generated/mysql.php",
|
||||
"generated/mysqli.php",
|
||||
"generated/network.php",
|
||||
"generated/oci8.php",
|
||||
"generated/opcache.php",
|
||||
"generated/openssl.php",
|
||||
"generated/outcontrol.php",
|
||||
"generated/pcntl.php",
|
||||
"generated/pcre.php",
|
||||
"generated/pgsql.php",
|
||||
"generated/posix.php",
|
||||
"generated/ps.php",
|
||||
"generated/pspell.php",
|
||||
"generated/readline.php",
|
||||
"generated/rnp.php",
|
||||
"generated/rpminfo.php",
|
||||
"generated/rrd.php",
|
||||
"generated/sem.php",
|
||||
"generated/session.php",
|
||||
"generated/shmop.php",
|
||||
"generated/sockets.php",
|
||||
"generated/sodium.php",
|
||||
"generated/solr.php",
|
||||
"generated/spl.php",
|
||||
"generated/sqlsrv.php",
|
||||
"generated/ssdeep.php",
|
||||
"generated/ssh2.php",
|
||||
"generated/stream.php",
|
||||
"generated/strings.php",
|
||||
"generated/swoole.php",
|
||||
"generated/uodbc.php",
|
||||
"generated/uopz.php",
|
||||
"generated/url.php",
|
||||
"generated/var.php",
|
||||
"generated/xdiff.php",
|
||||
"generated/xml.php",
|
||||
"generated/xmlrpc.php",
|
||||
"generated/yaml.php",
|
||||
"generated/yaz.php",
|
||||
"generated/zip.php",
|
||||
"generated/zlib.php"
|
||||
],
|
||||
"classmap": [
|
||||
"lib/DateTime.php",
|
||||
"lib/DateTimeImmutable.php",
|
||||
"lib/Exceptions/",
|
||||
"generated/Exceptions/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
||||
"support": {
|
||||
"issues": "https://github.com/thecodingmachine/safe/issues",
|
||||
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/OskarStark",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/shish",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/silasjoisten",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/staabm",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-04T18:08:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
"version": "v2.4.0",
|
||||
|
||||
@@ -22,9 +22,19 @@ RUN apt-get install -y nginx \
|
||||
libxml2-dev \
|
||||
libmagickwand-dev \
|
||||
imagemagick \
|
||||
ghostscript \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick
|
||||
|
||||
# ImageMagick blockt PDF-Lesen/-Schreiben per Default (Debian policy.xml).
|
||||
# Fuer den falsisign-Nachbau (PDF -> Raster -> PDF via imagick/ghostscript)
|
||||
# muessen PDF-Rechte auf read|write gesetzt werden.
|
||||
RUN for policy in /etc/ImageMagick-7/policy.xml /etc/ImageMagick-6/policy.xml; do \
|
||||
if [ -f "$policy" ]; then \
|
||||
sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' "$policy"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
#&& rm -rf /var/lib/apt/lists/* \
|
||||
RUN mkdir -p /run/nginx
|
||||
|
||||
@@ -35,6 +45,9 @@ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
|
||||
|
||||
COPY /docker/php/composer.phar /usr/bin/composer
|
||||
|
||||
# Upload-/Speicher-Limits fuer die Bildverarbeitung (zz- laedt zuletzt).
|
||||
COPY /docker/php/uploads.ini /usr/local/etc/php/conf.d/zz-uploads.ini
|
||||
|
||||
# Arbeitsverzeichnis
|
||||
WORKDIR /var/www/html
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ server {
|
||||
root /var/www/html/public;
|
||||
index index.php;
|
||||
|
||||
# Upload-Groesse (PDF bis 20 MB + Unterschrift/Stempel); muss zu PHP post_max_size passen.
|
||||
client_max_body_size 40m;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/nexus.access.log combined;
|
||||
error_log /var/log/nginx/nexus.error.log warn;
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
+6
-4
@@ -14,8 +14,9 @@ import {
|
||||
faStar, faClock, faCircleQuestion, faSliders,
|
||||
faBell, faMagnifyingGlass,
|
||||
faGraduationCap, faFileInvoice, faHeart, faCertificate,
|
||||
faChevronRight, faArrowRightFromBracket, faCube,
|
||||
faTriangleExclamation,
|
||||
faChevronRight, faChevronLeft, faArrowRightFromBracket, faCube,
|
||||
faTriangleExclamation, faPlus, faFilePdf, faDownload,
|
||||
faSignature, faStamp,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
@@ -24,8 +25,9 @@ library.add(
|
||||
faStar, faClock, faCircleQuestion, faSliders,
|
||||
faBell, faMagnifyingGlass,
|
||||
faGraduationCap, faFileInvoice, faHeart, faCertificate,
|
||||
faChevronRight, faArrowRightFromBracket, faCube,
|
||||
faTriangleExclamation,
|
||||
faChevronRight, faChevronLeft, faArrowRightFromBracket, faCube,
|
||||
faTriangleExclamation, faPlus, faFilePdf, faDownload,
|
||||
faSignature, faStamp,
|
||||
)
|
||||
|
||||
InertiaProgress.init()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title inertia>{{ config('app.name', 'Nexus') }}</title>
|
||||
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>Rechnung {{ $rechnungsnummer }}</title>
|
||||
<style>
|
||||
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 0;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
font-size: 9.5pt;
|
||||
color: #1a1a1a;
|
||||
background: white;
|
||||
}
|
||||
|
||||
|
||||
/* ── Kohte: steht zwischen Empfänger und Kontaktdaten ── */
|
||||
.kohte-img {
|
||||
width: 13mm;
|
||||
height: 13mm;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Seiteninhalt ── */
|
||||
.page {
|
||||
padding: 10mm 18mm 20mm 10mm;
|
||||
}
|
||||
|
||||
/* ── Kopfbereich ── */
|
||||
.header-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6mm;
|
||||
}
|
||||
.header-table td { vertical-align: top; padding: 0; }
|
||||
|
||||
/* Rücksendeadresse (schmale Linie über Empfänger) */
|
||||
.absender-rueck {
|
||||
font-size: 6pt;
|
||||
color: #555;
|
||||
border-bottom: 0.3pt solid #888;
|
||||
padding-bottom: 1.5mm;
|
||||
margin-bottom: 3.5mm;
|
||||
}
|
||||
|
||||
/* Empfängeradresse */
|
||||
.empfaenger { font-size: 10pt; line-height: 1.65; }
|
||||
.empfaenger-bold { font-weight: bold; }
|
||||
|
||||
/* ── BdP Logo ── */
|
||||
.logo-img {
|
||||
width: 36mm;
|
||||
display: block;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
/* Absenderadresse rechts (linksbündig) */
|
||||
.absender-block {
|
||||
font-size: 7.5pt;
|
||||
color: #1a4799;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.absender-name {
|
||||
font-weight: bold;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
/* ── Betreff ── */
|
||||
.betreff {
|
||||
font-size: 14pt;
|
||||
color: #1a4799;
|
||||
margin-bottom: 5mm;
|
||||
}
|
||||
|
||||
/* ── Referenzdaten ── */
|
||||
.ref-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
margin-bottom: 7mm;
|
||||
}
|
||||
.ref-table td { padding: 0.9mm 7mm 0.9mm 0; vertical-align: top; }
|
||||
.ref-key { color: #666; white-space: nowrap; }
|
||||
|
||||
/* ── Anrede / Text ── */
|
||||
.salutation { font-size: 9.5pt; margin-bottom: 3mm; }
|
||||
.intro-text { font-size: 9pt; line-height: 1.6; margin-bottom: 7mm; color: #333; }
|
||||
|
||||
/* ── Positionstabelle ── */
|
||||
.pos-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
.pos-table th {
|
||||
background-color: #1a4799;
|
||||
color: white;
|
||||
padding: 2.5mm 3mm;
|
||||
text-align: left;
|
||||
font-size: 7.5pt;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.pos-table th.r, .pos-table td.r { text-align: right; }
|
||||
.pos-table td {
|
||||
padding: 2mm 3mm;
|
||||
border-bottom: 0.3pt solid #d8dde6;
|
||||
vertical-align: top;
|
||||
}
|
||||
.pos-table tr:nth-child(even) td { background-color: #f7f8fb; }
|
||||
.pos-table tr:last-child td { border-bottom: none; }
|
||||
.pos-empty {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
padding: 3mm 0 !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* ── Summen ── */
|
||||
.sum-outer { width: 100%; border-collapse: collapse; margin-top: 1mm; margin-bottom: 5mm; }
|
||||
.sum-outer td { padding: 0; }
|
||||
.sum-spacer { width: 55%; }
|
||||
.sum-inner { width: 100%; border-collapse: collapse; font-size: 8.5pt; }
|
||||
.sum-inner td { padding: 1.2mm 0; }
|
||||
.sum-key { color: #555; padding-right: 6mm; white-space: nowrap; }
|
||||
.sum-val { text-align: right; white-space: nowrap; }
|
||||
.sum-line td { border-top: 0.4pt solid #bbb; padding-top: 2mm; }
|
||||
.sum-total td {
|
||||
font-weight: bold;
|
||||
font-size: 11pt;
|
||||
color: #1a4799;
|
||||
border-top: 1.5pt solid #1a4799;
|
||||
padding-top: 2mm;
|
||||
}
|
||||
|
||||
/* ── Bankverbindung ── */
|
||||
.bank-box {
|
||||
border-left: 3pt solid #f5c400;
|
||||
padding: 3mm 5mm;
|
||||
background-color: #fffef5;
|
||||
font-size: 8.5pt;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 6mm;
|
||||
}
|
||||
.bank-head {
|
||||
font-size: 7pt;
|
||||
font-weight: bold;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
.bank-hint {
|
||||
margin-top: 1.5mm;
|
||||
font-size: 7.5pt;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* ── Grußformel ── */
|
||||
.closing { font-size: 9.5pt; line-height: 1.5; }
|
||||
.gut-pfad { font-size: 13pt; color: #1a4799; margin-top: 1mm; margin-bottom: 1mm; }
|
||||
.closing-name { margin-top: 3mm; font-size: 9pt; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div class="page">
|
||||
|
||||
<!-- ══════════ KOPF ══════════ -->
|
||||
<table class="header-table">
|
||||
<tr>
|
||||
|
||||
<!-- LINKS: Rücksendezeile + Empfänger -->
|
||||
<td style="width: 50%; padding-top: 4mm;">
|
||||
<div class="absender-rueck">
|
||||
BdP Bund der Pfadfinder*innen LV Sachsen e.V. ·
|
||||
c/o Mandy Grazek · Weststraße 53c · 09212 Limbach-Oberfrohna
|
||||
</div>
|
||||
<div class="empfaenger">
|
||||
@if($organisation)
|
||||
<div class="empfaenger-bold">{{ $organisation }}</div>
|
||||
@endif
|
||||
@if($vorname || $nachname)
|
||||
<div class="empfaenger-bold">
|
||||
{{ trim($anrede . ' ' . $vorname . ' ' . $nachname) }}
|
||||
</div>
|
||||
@endif
|
||||
@if($adresse)
|
||||
<div>{{ $adresse }}</div>
|
||||
@endif
|
||||
@if($plz || $ort)
|
||||
<div>{{ trim($plz . ' ' . $ort) }}</div>
|
||||
@endif
|
||||
@if(!$organisation && !$vorname && !$nachname)
|
||||
<div style="color:#aaa;font-style:italic;">Kein Empfänger angegeben</div>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- MITTE: Kohte (zwischen Empfänger und Kontaktdaten) -->
|
||||
<td style="vertical-align: middle; padding-top: 4mm; width: 9%; padding-right: 20px;">
|
||||
<img src="{{ $kohteDataUri }}" style="width: 115px; height: 130px;" class="kohte-img" />
|
||||
</td>
|
||||
|
||||
<!-- RECHTS: BdP Logo + Kontaktdaten (linksbündig) -->
|
||||
<td style="width: 41%; vertical-align: top; padding-top: 2mm;">
|
||||
|
||||
<!-- BdP Wortmarke -->
|
||||
<img src="{{ $logoDataUri }}" class="logo-img" />
|
||||
|
||||
<!-- Absenderadresse -->
|
||||
<div class="absender-block">
|
||||
<div class="absender-name">Bund der Pfadfinder*innen</div>
|
||||
<div>Landesverband Sachsen e.V. (BdP)</div>
|
||||
<div>c/o Mandy Grazek</div>
|
||||
<div style="margin-top:2.5mm;">Weststraße 53c</div>
|
||||
<div>09212 Limbach‑Oberfrohna</div>
|
||||
<div style="margin-top:2.5mm;">Thomas Günther</div>
|
||||
<div style="margin-top:2.5mm;">{{ $absenderEmail }}</div>
|
||||
@if($absenderTelefon)
|
||||
<div>{{ $absenderTelefon }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ══════════ BETREFF ══════════ -->
|
||||
<div class="betreff">Rechnung Nr. {{ $rechnungsnummer }}</div>
|
||||
|
||||
<!-- ══════════ REFERENZDATEN ══════════ -->
|
||||
<table class="ref-table">
|
||||
<tr>
|
||||
<td class="ref-key">Rechnungsdatum:</td>
|
||||
<td>{{ $rechnungsdatumFormatiert }}</td>
|
||||
<td style="width: 8mm;"></td>
|
||||
<td class="ref-key">Rechnungs-Nr.:</td>
|
||||
<td>{{ $rechnungsnummer }}</td>
|
||||
</tr>
|
||||
@if($zahlungsziel)
|
||||
<tr>
|
||||
<td class="ref-key">Zahlungsziel:</td>
|
||||
<td colspan="4">{{ $zahlungszielFormatiert }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
|
||||
<!-- ══════════ ANREDE + EINLEITUNG ══════════ -->
|
||||
<div class="salutation">
|
||||
Hallo,
|
||||
</div>
|
||||
<div class="intro-text">
|
||||
anbei übersenden wir folgende Rechnungmit der Bitte um Überweisung des Gesamtbetrags
|
||||
bis zum {{ $zahlungszielFormatiert }} auf das unten angegebene Konto.
|
||||
</div>
|
||||
|
||||
<!-- ══════════ POSITIONEN ══════════ -->
|
||||
<table class="pos-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:8%;">Nr.</th>
|
||||
<th style="width:44%;">Bezeichnung</th>
|
||||
<th style="width:12%;" class="r">Menge</th>
|
||||
<th style="width:18%;" class="r">Einzelpreis</th>
|
||||
<th style="width:18%;" class="r">Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($positionen as $i => $pos)
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td>{{ $pos['bezeichnung'] }}</td>
|
||||
<td class="r">{{ number_format((float)($pos['menge'] ?? 0), 2, ',', '.') }} {{ $pos['einheit'] ?? '' }}</td>
|
||||
<td class="r">{{ number_format((float)($pos['einzelpreis'] ?? 0), 2, ',', '.') }} €</td>
|
||||
<td class="r">{{ number_format((float)($pos['menge'] ?? 0) * (float)($pos['einzelpreis'] ?? 0), 2, ',', '.') }} €</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="pos-empty">Keine Positionen erfasst.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ══════════ SUMMEN ══════════ -->
|
||||
<table class="sum-outer">
|
||||
<tr>
|
||||
<td class="sum-spacer"></td>
|
||||
<td>
|
||||
<table class="sum-inner">
|
||||
<tr>
|
||||
<td class="sum-key">Nettobetrag</td>
|
||||
<td class="sum-val">{{ number_format($netto, 2, ',', '.') }} €</td>
|
||||
</tr>
|
||||
<tr class="sum-line">
|
||||
<td class="sum-key">zzgl. {{ $mwstSatz }} % MwSt.</td>
|
||||
<td class="sum-val">{{ number_format($mwst, 2, ',', '.') }} €</td>
|
||||
</tr>
|
||||
<tr class="sum-total">
|
||||
<td class="sum-key">Gesamtbetrag</td>
|
||||
<td class="sum-val">{{ number_format($brutto, 2, ',', '.') }} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ══════════ BANKVERBINDUNG ══════════ -->
|
||||
@if($iban || $bic)
|
||||
<div class="bank-box">
|
||||
<div class="bank-head">Bankverbindung</div>
|
||||
<div>Kontoinhaber: BdP Landesverband Sachsen e.V.</div>
|
||||
@if($iban)<div>IBAN: <strong>{{ $iban }}</strong></div>@endif
|
||||
@if($bic)<div>BIC: <strong>{{ $bic }}</strong></div>@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- ══════════ GRUSSFORMEL ══════════ -->
|
||||
<div class="closing">
|
||||
<div class="gut-pfad">Herzlichst Gut Pfad.</div>
|
||||
<div class="closing-name">
|
||||
BdP Landesverband Sachsen e.V.<br />
|
||||
Landesschatzmeister<br />
|
||||
Thomas Günther</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,4 +7,5 @@ require_once __DIR__.'/../app/Domains/Schulbefreiung/Routes/web.php';
|
||||
require_once __DIR__.'/../app/Domains/Rechnung/Routes/web.php';
|
||||
require_once __DIR__.'/../app/Domains/Spendenquittung/Routes/web.php';
|
||||
require_once __DIR__.'/../app/Domains/Teilnahmebescheinigung/Routes/web.php';
|
||||
require_once __DIR__.'/../app/Domains/Unterschrift/Routes/web.php';
|
||||
require_once __DIR__.'/../app/Domains/Dummy/Routes/web.php';
|
||||
|
||||
Reference in New Issue
Block a user