diff --git a/.gitmodules b/.gitmodules index 9fde53df..3af5a3b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,6 @@ [submodule "packages/flysystem-webdav"] path = packages/flysystem-webdav url = https://github.com/zoomyboy/flysystem-webdav.git +[submodule "packages/table-document"] + path = packages/table-document + url = https://git.zoomyboy.de/zoomyboy/table-document.git diff --git a/app/Fileshare/Data/FileshareResourceData.php b/app/Fileshare/Data/FileshareResourceData.php index 11ceaa28..ffa77039 100644 --- a/app/Fileshare/Data/FileshareResourceData.php +++ b/app/Fileshare/Data/FileshareResourceData.php @@ -2,6 +2,8 @@ namespace App\Fileshare\Data; +use App\Fileshare\Models\Fileshare; +use Illuminate\Filesystem\FilesystemAdapter; use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Data; @@ -15,4 +17,14 @@ class FileshareResourceData extends Data public function __construct(public int $connectionId, public string $resource) { } + + public function getConnection(): Fileshare + { + return Fileshare::find($this->connectionId); + } + + public function getStorage(): FilesystemAdapter + { + return $this->getConnection()->type->getFilesystem(); + } } diff --git a/app/Form/Actions/ExportSyncAction.php b/app/Form/Actions/ExportSyncAction.php new file mode 100644 index 00000000..5c899baf --- /dev/null +++ b/app/Form/Actions/ExportSyncAction.php @@ -0,0 +1,87 @@ +form = $form; + + if (!$form->export->root) { + return; + } + + $storage = $form->export->root->getStorage(); + + $storage->put($form->export->root->resource . '/Anmeldungen ' . $form->name . '.xlsx', file_get_contents($this->allSheet($this->form->participants)->compile($this->tempPath()))); + + if ($form->export->toGroupField) { + foreach ($form->participants->groupBy(fn ($participant) => $participant->data[$form->export->toGroupField]) as $groupId => $participants) { + $group = Group::find($groupId); + if (!$group?->fileshare) { + continue; + } + + $group->fileshare->getStorage()->put($group->fileshare->resource . '/Anmeldungen ' . $form->name . '.xlsx', file_get_contents($this->allSheet($participants)->compile($this->tempPath()))); + } + } + } + + public function asJob(int $formId): void + { + $this->handle(Form::find($formId)); + } + + private function allSheet(Collection $participants): TableDocumentData + { + $document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]); + $headers = $this->form->getFields()->map(fn ($field) => $field->name)->toArray(); + + $document->addSheet(SheetData::from([ + 'header' => $headers, + 'data' => $participants + ->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray()) + ->toArray(), + 'name' => 'Alle', + ])); + + if ($this->form->export->groupBy) { + $groups = $participants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw()); + + foreach ($groups as $name => $participants) { + $document->addSheet(SheetData::from([ + 'header' => $headers, + 'data' => $participants + ->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray()) + ->toArray(), + 'name' => $name, + ])); + } + + $document->addSheet(SheetData::from([ + 'header' => ['Wert', 'Anzahl'], + 'data' => $groups->map(fn ($participants, $name) => [$name, (string) count($participants)])->toArray(), + 'name' => 'Statistik', + ])); + } + + return $document; + } + + private function tempPath(): string + { + return sys_get_temp_dir() . '/' . str()->uuid()->toString(); + } +} diff --git a/app/Form/Actions/RegisterAction.php b/app/Form/Actions/RegisterAction.php index c7378b5f..f4f513c9 100644 --- a/app/Form/Actions/RegisterAction.php +++ b/app/Form/Actions/RegisterAction.php @@ -32,6 +32,7 @@ class RegisterAction $form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input)); $participant->sendConfirmationMail(); + ExportSyncAction::dispatch($form->id); return $participant; } diff --git a/app/Form/Fields/Field.php b/app/Form/Fields/Field.php index 8ae68ded..82f23f06 100644 --- a/app/Form/Fields/Field.php +++ b/app/Form/Fields/Field.php @@ -104,10 +104,7 @@ abstract class Field extends Data ]; } - /** - * @return mixed - */ - public function presentRaw() + public function presentRaw(): string { return $this->getPresenter()->present($this->value); } diff --git a/composer.json b/composer.json index 7c2a4e52..929be98d 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,13 @@ "symlink": true } }, + { + "type": "path", + "url": "./packages/table-document", + "options": { + "symlink": true + } + }, { "type": "path", "url": "./packages/flysystem-webdav", @@ -69,6 +76,7 @@ "league/flysystem-webdav": "dev-master as 3.28.0", "zoomyboy/osm": "1.0.3", "zoomyboy/phone": "^1.0", + "zoomyboy/table-document": "dev-master as 1.0", "zoomyboy/tex": "dev-main as 1.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 12d9c632..4174cbf3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "552ce1f9c6448e20c08c2b3baca394df", + "content-hash": "7541aa772bfd34e379ec31ce8cefdb58", "packages": [ { "name": "amphp/amp", @@ -5205,6 +5205,113 @@ ], "time": "2023-06-21T14:59:35+00:00" }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "maximebf/debugbar", "version": "v1.22.3", @@ -7136,6 +7243,110 @@ }, "time": "2024-02-23T11:10:43+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e", + "reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.1.0" + }, + "time": "2024-05-11T04:17:56+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.2", @@ -14154,6 +14365,37 @@ "description": "Phone number formatting", "time": "2023-03-02T23:22:08+00:00" }, + { + "name": "zoomyboy/table-document", + "version": "dev-master", + "dist": { + "type": "path", + "url": "./packages/table-document", + "reference": "c478784bbb26d7dc28c08670322dbf3b0d845f8c" + }, + "require": { + "laravel/framework": "^9.0", + "phpoffice/phpspreadsheet": "^2.1", + "spatie/laravel-data": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zoomyboy\\TableDocument\\": "src/" + } + }, + "authors": [ + { + "name": "Philipp Lang", + "email": "philipp@zoomyboy.de" + } + ], + "description": "Table document creator", + "transport-options": { + "symlink": true, + "relative": true + } + }, { "name": "zoomyboy/tex", "version": "dev-main", @@ -15877,6 +16119,12 @@ "alias": "1.0", "alias_normalized": "1.0.0.0" }, + { + "package": "zoomyboy/table-document", + "version": "9999999-dev", + "alias": "1.0", + "alias_normalized": "1.0.0.0" + }, { "package": "zoomyboy/tex", "version": "dev-main", @@ -15889,6 +16137,7 @@ "zoomyboy/laravel-nami": 20, "zoomyboy/medialibrary-helper": 20, "league/flysystem-webdav": 20, + "zoomyboy/table-document": 20, "zoomyboy/tex": 20 }, "prefer-stable": true, diff --git a/packages/table-document b/packages/table-document new file mode 160000 index 00000000..c1d0221d --- /dev/null +++ b/packages/table-document @@ -0,0 +1 @@ +Subproject commit c1d0221dcd2b4200b3ff17747e31f451fcc749f0 diff --git a/tests/Feature/Form/FormRegisterActionTest.php b/tests/Feature/Form/FormRegisterActionTest.php index f968252a..5f5505a7 100644 --- a/tests/Feature/Form/FormRegisterActionTest.php +++ b/tests/Feature/Form/FormRegisterActionTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Form; +use App\Form\Actions\ExportAction; +use App\Form\Actions\ExportSyncAction; use App\Form\Enums\NamiType; use App\Form\Enums\SpecialType; use App\Form\Mails\ConfirmRegistrationMail; @@ -41,6 +43,7 @@ class FormRegisterActionTest extends FormTestCase ]), ]) ->create(); + ExportSyncAction::shouldRun()->once()->with($form->id); $this->register($form, ['vorname' => 'Max', 'nachname' => 'Muster', 'spitzname' => 'Abraham']) ->assertOk(); diff --git a/tests/Fileshare/ExportSyncActionTest.php b/tests/Fileshare/ExportSyncActionTest.php new file mode 100644 index 00000000..19034173 --- /dev/null +++ b/tests/Fileshare/ExportSyncActionTest.php @@ -0,0 +1,70 @@ +fields([ + $this->textField('vorname'), + $this->textField('nachname'), + ])->create(); + + ExportSyncAction::run($form); + $this->assertTrue(true); + } + + public function testItUploadsRootFile(): void + { + $this->withoutExceptionHandling()->withOwncloudUser('badenpowell', 'secret')->withDirs('badenpowell', ['/abc']); + $connection = Fileshare::factory() + ->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')])) + ->create(); + $form = Form::factory()->name('Formular')->fields([ + $this->textField('vorname'), + $this->textField('nachname'), + ])->export(ExportData::from(['root' => FileshareResourceData::from(['connection_id' => $connection->id, 'resource' => '/abc'])]))->create(); + Participant::factory()->for($form)->data(['firstname' => 'AAA', 'lastname' => 'BBB'])->create(); + + ExportSyncAction::run($form); + + $this->assertEquals(['abc/Anmeldungen Formular.xlsx'], $connection->type->getFilesystem()->files('/abc')); + $this->assertTrue(true); + } + + public function testItUploadsGroupFile(): void + { + $this->withoutExceptionHandling()->withOwncloudUser('badenpowell', 'secret')->withDirs('badenpowell', ['/abc', '/stamm']); + $connection = Fileshare::factory() + ->type(OwncloudConnection::from(['user' => 'badenpowell', 'password' => 'secret', 'base_url' => env('TEST_OWNCLOUD_DOMAIN')])) + ->create(); + $group = Group::factory()->create(['fileshare' => FileshareResourceData::from(['connection_id' => $connection->id, 'resource' => '/stamm'])]); + $form = Form::factory()->name('Formular')->fields([ + $this->textField('vorname')->name('Vorname'), + $this->textField('nachname')->name('Nachname'), + $this->groupField('stamm')->name('Stamm'), + ])->export(ExportData::from(['to_group_field' => 'stamm', 'group_by' => 'vorname', 'root' => FileshareResourceData::from(['connection_id' => $connection->id, 'resource' => '/abc'])]))->create(); + Participant::factory()->for($form)->data(['vorname' => 'AAA', 'nachname' => 'BBB', 'stamm' => $group->id])->create(); + Participant::factory()->for($form)->data(['vorname' => 'CCC', 'nachname' => 'DDD', 'stamm' => null])->create(); + + ExportSyncAction::run($form); + + $this->assertEquals(['stamm/Anmeldungen Formular.xlsx'], $connection->type->getFilesystem()->files('/stamm')); + $this->assertTrue(true); + } +}