diff --git a/app/Enums/Claims/ClaimStatus.php b/app/Enums/Claims/ClaimStatus.php index 53d2c64..afa8e99 100644 --- a/app/Enums/Claims/ClaimStatus.php +++ b/app/Enums/Claims/ClaimStatus.php @@ -12,6 +12,7 @@ enum ClaimStatus: int case sentDigital = 2; case sentMarketing = 3; case treated = 4; + case refused = 5; /** * Optionnal labels definition @@ -21,6 +22,7 @@ enum ClaimStatus: int self::sentDigital->name => 'Transférée au service Digital', self::sentMarketing->name => 'Retournée au Marketing', self::treated->name => 'Traitée', + self::refused->name => 'Refusé de répondre', ]; /** @@ -31,6 +33,7 @@ enum ClaimStatus: int self::sentDigital->name => '#0033c0', self::sentMarketing->name => '#69077c', self::treated->name => '#188618', + self::refused->name => '#dc143c', ]; /** diff --git a/app/Enums/ExportFormat.php b/app/Enums/ExportFormat.php new file mode 100644 index 0000000..7d9731a --- /dev/null +++ b/app/Enums/ExportFormat.php @@ -0,0 +1,85 @@ +name => 'csv', + self::csvWithoutAttachment->name => 'csv sans pièces jointes', + self::pdf->name => 'pdf', + ]; + + /** + * Bootstrap color class to use for the export buttons. + * + * @return string + */ + public function class(): string + { + return match ($this) { + self::csv, + self::csvWithoutAttachment => 'success', + self::pdf => 'danger', + default => 'primary', + }; + } + + /** + * Font Awesome picto to use for the export buttons. + * + * @return string + */ + public function picto(): string + { + return match ($this) { + self::csv, + self::csvWithoutAttachment => 'fa-solid fa-file-csv', + self::pdf => 'fa-solid fa-file-pdf', + default => 'fa-solid fa-file', + }; + } + + + /** + * File headers to use when the exported file is return with StreamedResponse. + * + * @return array + */ + public function fileHeaders(): array + { + return match ($this) { + self::csv, + self::csvWithoutAttachment => [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="export.csv"', + ], + default => [], + }; + } + + /** + * Export file name. + * + * @return string + */ + public function fileName(): string + { + return match ($this) { + self::csv, + self::csvWithoutAttachment => 'export.csv', + self::pdf => 'export.pdf', + }; + } +} diff --git a/app/Http/Controllers/Api/V4/ClaimController.php b/app/Http/Controllers/Api/V4/ClaimController.php index 784127c..f2e5fd2 100644 --- a/app/Http/Controllers/Api/V4/ClaimController.php +++ b/app/Http/Controllers/Api/V4/ClaimController.php @@ -24,7 +24,7 @@ class ClaimController extends Controller { $claimModel = (new Claim())->fill(Arr::except($request->validated(), 'file')); if ($request->filled('file')) { - $claimModel->file = FileHelper::storeBase64File('claim', $request->file); + $claimModel->file = FileHelper::storeBase64File('claim', $request->file, private: true); } if (!$claimModel->saveOrFail()) { diff --git a/app/Http/Controllers/Bo/ClaimController.php b/app/Http/Controllers/Bo/ClaimController.php index 1fcde8a..9bb94e8 100644 --- a/app/Http/Controllers/Bo/ClaimController.php +++ b/app/Http/Controllers/Bo/ClaimController.php @@ -4,9 +4,11 @@ namespace App\Http\Controllers\Bo; use App\Enums\Claims\ClaimResponseType; use App\Enums\Claims\ClaimStatus; +use App\Enums\ExportFormat; use App\Enums\Users\RoleEnum; use App\Events\ClaimModelChanged; use App\Http\Controllers\Controller; +use App\Http\Requests\Bo\Claims\ExportClaimRequest; use App\Http\Requests\Bo\Claims\SendClaimAnswerRequest; use App\Http\Requests\Bo\Claims\UpdateClaimRequest; use App\Http\Requests\Bo\PaginateModelsRequest; @@ -17,15 +19,18 @@ use App\Models\ClaimMailTemplate; use App\Models\ClaimMessage; use App\Models\ClaimSubject; use App\Models\CustomerInfo; +use App\Traits\Controllers\ExportDatas; use App\Traits\Controllers\UpdateModelPublished; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; +use Symfony\Component\HttpFoundation\StreamedResponse; use Uwil\LaravelCommon\Helpers\CollectionHelper; class ClaimController extends Controller { use UpdateModelPublished; + use ExportDatas; /** * Create the controller instance. @@ -54,6 +59,15 @@ class ClaimController extends Controller )->when( $userModel->role->equals(RoleEnum::marketing), fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys())) + ) + ->when( + $request->filled('subject'), + fn($q) => $q->whereHas('claimSubject', fn($q) => $q->where('id', $request->subject)) + ) + ->when( + $request->filled('status'), + fn($q) => $q->whereIn('status', $request->status), + fn($q) => $q->whereNotIn('status', [ClaimStatus::treated, ClaimStatus::refused]) ); /** @var string $search Search field */ @@ -82,7 +96,15 @@ class ClaimController extends Controller /** Custom pagination */ $claimModels = $query->simplePaginate($this->getPagination()); - return view('bo.pages.claims.index', compact('claimModels', 'search', 'searchFields')); + $claimSubjectModels = ClaimSubject::query() + ->when( + $request->filled('brand'), + fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand)), + fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands()->pluck('id'))) + ) + ->get(); + + return view('bo.pages.claims.index', compact('claimModels', 'search', 'searchFields', 'claimSubjectModels')); } /** @@ -204,4 +226,39 @@ class ClaimController extends Controller ); return \response()->json($models); } + + + // * EXPORT + + /** + * Export claim datas to a given format + * + * @param ExportClaimRequest $request + * @return \Illuminate\Http\Response|Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse + */ + public function export(ExportClaimRequest $request) + { + /** @var \Illuminate\Database\Eloquent\Collection $claims*/ + $claims = Claim::query()->whereIn('id', $request->validated('claimList'))->get(); + $format = ExportFormat::from($request->validated('format')); + + // * CSV + if ($format->equals(ExportFormat::csvWithoutAttachment, ExportFormat::csv)) { + $csv = $this->generateCsv($claims); + if (!$this->needsZip($format, $claims)) { + return new StreamedResponse(function () use ($format, $csv) { + echo $csv; + }, 200, $this->getFileHeaders($format)); + } + return $this->returnExportZip($format, $claims, $csv); + // * PDF + } elseif ($format->equals(ExportFormat::pdf)) { + $pdf = $this->generatePdf($claims); + + if (!$this->needsZip(ExportFormat::pdf, $claims)) { + return $pdf->download('export.pdf'); + } + return $this->returnExportZip($format, $claims, $pdf->output()); + }; //end if + } } diff --git a/app/Http/Requests/Bo/Claims/ExportClaimRequest.php b/app/Http/Requests/Bo/Claims/ExportClaimRequest.php new file mode 100644 index 0000000..908836f --- /dev/null +++ b/app/Http/Requests/Bo/Claims/ExportClaimRequest.php @@ -0,0 +1,65 @@ +check(); + } + + /** + * Prepare the data for validation. + * + * @return void + */ + public function prepareForValidation() + { + $this->merge([ + 'claimList' => json_decode($this->input('claimList'), true) + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + return [ + 'format' => [ + 'required', + Rule::enum(ExportFormat::class) + ], + 'claimList' => 'required|array', + 'claimList.*' => 'required|numeric|exists:claims,id', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array|null> + */ + public function attributes(): array + { + return [ + 'brand_id' => trans('models.classes.brands'), + 'subject_id' => trans('models.classes.claimsubjects'), + 'type' => trans('validation.custom.type'), + 'status' => trans('validation.custom.status'), + 'response_type' => trans('validation.custom.response_type'), + ]; + } +} diff --git a/app/Interfaces/IsExportable.php b/app/Interfaces/IsExportable.php new file mode 100644 index 0000000..ad6cba5 --- /dev/null +++ b/app/Interfaces/IsExportable.php @@ -0,0 +1,34 @@ +replaceFirst('storage/app/private', ''); + } +} diff --git a/app/Models/Claim.php b/app/Models/Claim.php index 1a23fda..eeb4559 100644 --- a/app/Models/Claim.php +++ b/app/Models/Claim.php @@ -6,8 +6,10 @@ use App\Enums\Claims\ClaimResponseType; use App\Enums\Claims\ClaimStatus; use App\Enums\Claims\ClaimType; use App\Events\ClaimModelChanged; +use App\Interfaces\IsExportable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\File; use Illuminate\Support\Str; /** @@ -29,8 +31,11 @@ use Illuminate\Support\Str; * @property string|null $hashcode Claim associated Customer hashcode. * @property-read \Illuminate\Support\Carbon $created_at Claim created date. * @property-read \Illuminate\Support\Carbon $updated_at Claim updated date. + * + * @property-read string $file_extension Extension of the file. + * @property-read string $export_file_name Name of the exported file. */ -class Claim extends Model +class Claim extends Model implements IsExportable { use HasFactory; @@ -199,4 +204,76 @@ class Claim extends Model trim(implode(' ', array_filter([$this->postcode, $this->city]))) ])); } + + /** + * Get the formmated csv line for the current model. + * + * @return array + */ + public function getFormattedCsvLine(): array + { + return [ + $this->reference, + $this->created_at->toDateTimeString(), + sprintf('%s %s', $this->first_name, Str::upper($this->last_name)), + $this->email, + sprintf('%s, %s, %s', $this->address, $this->postcode, $this->city), + $this->status->label(), + $this->claimSubject->label, + $this->type->label(), + $this->brand->name, + $this->message, + $this->export_file_name, + ]; + } + + /** + * Get the csv headers for this model. + * + * @return array + */ + public static function getCsvHeaders(): array + { + return [ + 'Référence', + 'Date de création', + 'Prénom NOM', + 'Email', + 'Adresse', + 'Statut', + 'Sujet', + 'Type', + 'Marque', + 'Message', + 'Pièce jointe', + ]; + } + + // * VIRTUAL ATTRIBUTES + + /** + * 'file_extension' virtual property. + * + * @return string + */ + public function getFileExtensionAttribute(): string + { + return File::extension($this->file); + } + + /** + * 'export_file_name' virtual property. + * + * @return string + */ + public function getExportFileNameAttribute(): string + { + return $this->file ? + sprintf( + '%s.%s', + $this->reference, + $this->file_extension, + ) : + __('Aucune pièce jointe'); + } } diff --git a/app/Policies/ClaimPolicy.php b/app/Policies/ClaimPolicy.php index a6ea601..438594c 100644 --- a/app/Policies/ClaimPolicy.php +++ b/app/Policies/ClaimPolicy.php @@ -106,6 +106,18 @@ class ClaimPolicy return $this->checkOnlyMinAdminOrInUserBrands($authUser, $claim); } + /** + * Export a Claim. + * + * @param \App\Models\User $authUser + * @param \App\Models\Claim $claim + * @return boolean + */ + public function export(User $authUser, Claim $claim): bool + { + return $this->checkOnlyMinAdminOrInUserBrands($authUser, $claim); + } + /** * Check if the user an access a specific resource. * diff --git a/app/Traits/Controllers/ExportDatas.php b/app/Traits/Controllers/ExportDatas.php new file mode 100644 index 0000000..82da1b5 --- /dev/null +++ b/app/Traits/Controllers/ExportDatas.php @@ -0,0 +1,229 @@ +equals(ExportFormat::csv)) { + return $models->whereNotNull($fileFieldName)->pluck($fileFieldName)->isNotEmpty(); + } elseif ($format->equals(ExportFormat::pdf)) { + return $models + ->whereNotNull($fileFieldName) + ->whereNotIn('file_extension', ['jpg', 'jpeg', 'png']) + ->isNotEmpty(); + } + + return false; + } + + /** + * Return a StreamedResponse with a zip file. + * + * @param ExportFormat $format + * @param \Illuminate\Database\Eloquent\Collection $models + * @param string $file + * @return \Symfony\Component\HttpFoundation\StreamedResponse + */ + public function returnExportZip( + ExportFormat $format, + \Illuminate\Database\Eloquent\Collection $models, + string $file + ): \Symfony\Component\HttpFoundation\StreamedResponse { + return new StreamedResponse(function () use ($file, $models, $format) { + // Create zip. + $zip = new ZipArchive(); + $zipName = $this->createExportZip($zip, $file, $format); + + // Add attachments. + $this->addAttachmentsToZip($zip, $models, $format); + + $zip->close(); + readfile($zipName); + unlink($zipName); + }, 200, $this->getFileHeaders($format, true)); + } + + /** + * Create the zip folder for the export with the main file inside. + * + * @param ZipArchive $zip + * @param string $file + * @param ExportFormat $format + * @return string|false + * + * @throws \Exception If the zip opening failed. + */ + private function createExportZip(ZipArchive &$zip, string $file, ExportFormat $format): string|false + { + $zipName = tempnam(Storage::disk('private')->path('export'), 'zip'); + if (!$zip->open($zipName, ZipArchive::OVERWRITE)) { + throw new \Exception(__('Couldn\'t open zip file.')); + }; + $zip->addFromString($format->fileName(), $file); + + return $zipName; + } + + /** + * Add attachments to zip + * + * @param ZipArchive $zip + * @param \Illuminate\Database\Eloquent\Collection $models + * @param ExportFormat $format + * @param string $fileFieldName Name of the field containing the file. + * @return void + */ + private function addAttachmentsToZip( + ZipArchive &$zip, + \Illuminate\Database\Eloquent\Collection $models, + ExportFormat $format, + string $fileFieldName = 'file' + ) { + $modelsWithFilesToZip = $models->whereNotNull($fileFieldName) + ->when($format->equals(ExportFormat::pdf), function ($models) { + return $models->whereNotIn('file_extension', ['jpg', 'jpeg', 'png']); + }); + foreach ($modelsWithFilesToZip as $model) { + $filePath = FileHelper::getPrivateFilePath($model->{$fileFieldName}); + if (Storage::disk('private')->exists($filePath)) { + $zip->addFile( + Storage::disk('private')->path($filePath), + $model->export_file_name + ); + } + } + } + + /** + * Get file headers to use with StreamedResponse. + * + * @param ExportFormat $format + * @param boolean $isZip + * @return array + */ + public function getFileHeaders(ExportFormat $format, bool $isZip = false): array + { + if ($isZip) { + return [ + 'Content-Type' => 'application/zip', + 'Content-Disposition' => 'attachment; filename="export.zip"', + ]; + }; + return $format->fileHeaders(); + } + + /** + * Transform Array into CSV. + * + * @param array $lines + * @param string $delimiter + * @param string $enclosure + * @param string $escape_char + * @return string|false + */ + private function array2csv( + array $lines, + string $delimiter = ',', + string $enclosure = '"', + string $escape_char = "\\" + ) { + $f = fopen('php://temp', 'r+'); + + // UTF-8 BOM. + $BOM = "\xEF\xBB\xBF"; + fwrite($f, $BOM); + + foreach ($lines as $item) { + fputcsv($f, $item, $delimiter, $enclosure, $escape_char); + } + + rewind($f); + $csvContent = stream_get_contents($f); + fclose($f); + + return $csvContent; + } + + /** + * Generate CSV with given models. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @return string|false + */ + private function generateCsv(\Illuminate\Database\Eloquent\Collection $models) + { + // CSV Header. + $class = get_class($models->first()); + $csvlines[] = $class::getCsvHeaders(); + + foreach ($models as $model) { + $csvlines[] = $model->getFormattedCsvLine(); + } + + return $this->array2csv($csvlines, ';'); + } + + /** + * Generate PDF with models datas. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @param string $fileFieldName + * @return \Barryvdh\DomPDF\PDF + */ + private function generatePdf( + \Illuminate\Database\Eloquent\Collection $models, + string $fileFieldName = 'file' + ): \Barryvdh\DomPDF\PDF { + // Convert pictures to base64. + foreach ($models->whereIn('file_extension', ['jpg', 'jpeg', 'png']) as $model) { + $filePath = FileHelper::getPrivateFilePath($model->{$fileFieldName}); + $model->fileBase64 = sprintf( + 'data:%s;base64,%s', + File::mimeType(Storage::disk('private')->path($filePath)), + base64_encode(Storage::disk('private')->get($filePath)) + ); + } + + // Create temp folder if it doesn't exists. Needed to allow base64 pictures to be displayed in pdf. + if (!Storage::disk('private')->exists('temp')) { + Storage::disk('private')->makeDirectory('temp'); + } + + $modelClassName = class_basename($models->first()); + return Pdf::setOptions(['tempDir' => Storage::disk('private')->path('temp')]) + ->loadView( + sprintf( + 'export.pdf', + str($modelClassName)->lower()->plural() + ), + [ + 'models' => $models, + 'modelName' => str($modelClassName)->lower() + ] + ); + } +} diff --git a/composer.json b/composer.json index 4d1d30c..96e5378 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "24slides/laravel-saml2": "^2.4", "arondeparon/laravel-request-sanitizer": "^5.0", "axlon/laravel-postal-code-validation": "^3.2", + "barryvdh/laravel-dompdf": "^3.1", "diglactic/laravel-breadcrumbs": "^9.0", "doctrine/dbal": "^3.0", "doctrine/inflector": "^2.0", diff --git a/composer.lock b/composer.lock index b076c98..b1e042f 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": "c078e9a537be5c78427890f22fc24612", + "content-hash": "f04361d9a00c60112340a92b68293a68", "packages": [ { "name": "24slides/laravel-saml2", @@ -210,6 +210,83 @@ }, "time": "2024-09-10T15:23:59+00:00" }, + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9|^10", + "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.1" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-02-13T15:07:54+00:00" + }, { "name": "brick/math", "version": "0.12.1", @@ -995,6 +1072,161 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623", + "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.4" + }, + "time": "2025-10-29T12:43:30+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "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.1" + }, + "time": "2024-12-02T14:37:59+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "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.0" + }, + "time": "2024-04-29T13:26:35+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.4.0", @@ -3330,6 +3562,73 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "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.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, { "name": "mews/purifier", "version": "3.4.2", @@ -5115,6 +5414,72 @@ }, "time": "2024-11-20T21:13:56+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v8.9.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", + "rawr/cross-data-providers": "^2.0.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, + "autoload": { + "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/v8.9.0" + }, + "time": "2025-07-11T13:20:48+00:00" + }, { "name": "sentry/sentry", "version": "4.10.0", diff --git a/config/dompdf.php b/config/dompdf.php new file mode 100644 index 0000000..abe6f16 --- /dev/null +++ b/config/dompdf.php @@ -0,0 +1,304 @@ + true, + + // Override the public path if needed. + 'public_path' => null, + + // Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £. + 'convert_entities' => true, + + 'options' => [ + /** + * The location of the DOMPDF font directory + * + * The location of the directory where DOMPDF will store fonts and font metrics + * Note: This directory must exist and be writable by the webserver process. + * *Please note the trailing slash.* + * + * Notes regarding fonts: + * Additional .afm font metrics can be added by executing load_font.php from command line. + * + * Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must + * be embedded in the pdf file or the PDF may not display correctly. This can significantly + * increase file size unless font subsetting is enabled. Before embedding a font please + * review your rights under the font license. + * + * Any font specification in the source HTML is translated to the closest font available + * in the font directory. + * + * The pdf standard "Base 14 fonts" are: + * Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique, + * Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique, + * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, + * Symbol, ZapfDingbats. + */ + + // Advised by dompdf (https://github.com/dompdf/dompdf/pull/782). + 'font_dir' => storage_path('fonts'), + + /** + * The location of the DOMPDF font cache directory + * + * This directory contains the cached font metrics for the fonts used by DOMPDF. + * This directory can be the same as DOMPDF_FONT_DIR + * + * Note: This directory must exist and be writable by the webserver process. + */ + 'font_cache' => storage_path('fonts'), + + /** + * The location of a temporary directory. + * + * The directory specified must be writeable by the webserver process. + * The temporary directory is required to download remote images and when + * using the PDFLib back end. + */ + 'temp_dir' => ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(), + + /** + * ==== IMPORTANT ==== + * + * dompdf's "chroot": Prevents dompdf from accessing system files or other + * files on the webserver. All local files opened by dompdf must be in a + * subdirectory of this directory. DO NOT set it to '/' since this could + * allow an attacker to use dompdf to read any files on the server. This + * should be an absolute path. + * This is only checked on command line call by dompdf.php, but not by + * direct class use like: + * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output(); + */ + 'chroot' => realpath(base_path()), + + /** + * Protocol whitelist + * + * Protocols and PHP wrappers allowed in URIs, and the validation rules + * that determine if a resouce may be loaded. Full support is not guaranteed + * for the protocols/wrappers specified + * by this array. + * + * @var array + */ + 'allowed_protocols' => [ + 'data://' => ['rules' => []], + 'file://' => ['rules' => []], + 'http://' => ['rules' => []], + 'https://' => ['rules' => []], + ], + + /** + * Operational artifact (log files, temporary files) path validation + */ + 'artifactPathValidation' => null, + + /** + * @var string + */ + 'log_output_file' => null, + + /** + * Whether to enable font subsetting or not. + */ + 'enable_font_subsetting' => false, + + /** + * The PDF rendering backend to use + * + * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and + * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will + * fall back on CPDF. 'GD' renders PDFs to graphic files. + * {@link * Canvas_Factory} ultimately determines which rendering class to + * instantiate based on this setting. + * + * Both PDFLib & CPDF rendering backends provide sufficient rendering + * capabilities for dompdf, however additional features (e.g. object, + * image and font support, etc.) differ between backends. Please see + * {@link PDFLib_Adapter} for more information on the PDFLib backend + * and {@link CPDF_Adapter} and lib/class.pdf.php for more information + * on CPDF. Also see the documentation for each backend at the links + * below. + * + * The GD rendering backend is a little different than PDFLib and + * CPDF. Several features of CPDF and PDFLib are not supported or do + * not make any sense when creating image files. For example, + * multiple pages are not supported, nor are PDF 'objects'. Have a + * look at {@link GD_Adapter} for more information. GD support is + * experimental, so use it at your own risk. + * + * @link http://www.pdflib.com + * @link http://www.ros.co.nz/pdf + * @link http://www.php.net/image + */ + 'pdf_backend' => 'CPDF', + + /** + * html target media view which should be rendered into pdf. + * List of types and parsing rules for future extensions: + * http://www.w3.org/TR/REC-html40/types.html + * screen, tty, tv, projection, handheld, print, braille, aural, all + * Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3. + * Note, even though the generated pdf file is intended for print output, + * the desired content might be different (e.g. screen or projection view of html file). + * Therefore allow specification of content here. + */ + 'default_media_type' => 'screen', + + /** + * The default paper size. + * + * North America standard is "letter"; other countries generally "a4" + * + * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.) + */ + 'default_paper_size' => 'a4', + + /** + * The default paper orientation. + * + * The orientation of the page (portrait or landscape). + * + * @var string + */ + 'default_paper_orientation' => 'portrait', + + /** + * The default font family + * + * Used if no suitable fonts can be found. This must exist in the font folder. + * + * @var string + */ + 'default_font' => 'serif', + + /** + * Image DPI setting + * + * This setting determines the default DPI setting for images and fonts. The + * DPI may be overridden for inline images by explictly setting the + * image's width & height style attributes (i.e. if the image's native + * width is 600 pixels and you specify the image's width as 72 points, + * the image will have a DPI of 600 in the rendered PDF. The DPI of + * background images can not be overridden and is controlled entirely + * via this parameter. + * + * For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI). + * If a size in html is given as px (or without unit as image size), + * this tells the corresponding size in pt. + * This adjusts the relative sizes to be similar to the rendering of the + * html page in a reference browser. + * + * In pdf, always 1 pt = 1/72 inch + * + * Rendering resolution of various browsers in px per inch: + * Windows Firefox and Internet Explorer: + * SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:? + * Linux Firefox: + * about:config *resolution: Default:96 + * (xorg screen dimension in mm and Desktop font dpi settings are ignored) + * + * Take care about extra font/image zoom factor of browser. + * + * In images, size in pixel attribute, img css style, are overriding + * the real image dimension in px for rendering. + * + * @var int + */ + 'dpi' => 96, + + /** + * Enable embedded PHP + * + * If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained + * within tags. + * + * ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages) + * is a security risk. + * Embedded scripts are run with the same level of system access available to dompdf. + * Set this option to false (recommended) if you wish to process untrusted documents. + * This setting may increase the risk of system exploit. + * Do not change this settings without understanding the consequences. + * Additional documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + 'enable_php' => false, + + /** + * Rnable inline JavaScript + * + * If this setting is set to true then DOMPDF will automatically insert JavaScript code contained + * within tags as written into the PDF. + * NOTE: This is PDF-based JavaScript to be executed by the PDF viewer, + * not browser-based JavaScript executed by Dompdf. + * + * @var bool + */ + 'enable_javascript' => true, + + /** + * Enable remote file access + * + * If this setting is set to true, DOMPDF will access remote sites for + * images and CSS files as required. + * + * ==== IMPORTANT ==== + * This can be a security risk, in particular in combination with isPhpEnabled and + * allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...); + * This allows anonymous users to download legally doubtful internet content which on + * tracing back appears to being downloaded by your server, or allows malicious php code + * in remote html pages to be executed by your server with your account privileges. + * + * This setting may increase the risk of system exploit. Do not change + * this settings without understanding the consequences. Additional + * documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + 'enable_remote' => false, + + /** + * List of allowed remote hosts + * + * Each value of the array must be a valid hostname. + * + * This will be used to filter which resources can be loaded in combination with + * isRemoteEnabled. If enable_remote is FALSE, then this will have no effect. + * + * Leave to NULL to allow any remote host. + * + * @var array|null + */ + 'allowed_remote_hosts' => null, + + /** + * A ratio applied to the fonts height to be more like browsers' line height + */ + 'font_height_ratio' => 1.1, + + /** + * Use the HTML5 Lib parser + * + * @deprecated This feature is now always on in dompdf 2.x + * + * @var bool + */ + 'enable_html5_parser' => true, + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php index e8833b8..521ca82 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -67,7 +67,6 @@ return [ 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => false, ], - ], /* diff --git a/resources/lang/en/crud.php b/resources/lang/en/crud.php index 1579b61..9c9ce57 100644 --- a/resources/lang/en/crud.php +++ b/resources/lang/en/crud.php @@ -10,6 +10,7 @@ return [ 'delete' => 'delete', 'save' => 'save', 'edit' => 'edit', + 'export' => 'export', ], 'helpers' => [ diff --git a/resources/lang/fr/crud.php b/resources/lang/fr/crud.php index 7e9529a..02d3b76 100644 --- a/resources/lang/fr/crud.php +++ b/resources/lang/fr/crud.php @@ -10,6 +10,7 @@ return [ 'delete' => 'supprimer', 'save' => 'sauvegarder', 'edit' => 'mettre à jour', + 'export' => 'exporter', ], 'helpers' => [ diff --git a/resources/ts/bo/bo.ts b/resources/ts/bo/bo.ts index 23b50f3..7325c36 100644 --- a/resources/ts/bo/bo.ts +++ b/resources/ts/bo/bo.ts @@ -14,6 +14,7 @@ import "./../modules/sweetalert"; import "./base/common"; // CUSTOM +import "./pages/claims"; import "./pages/customer.delete"; import "./pages/data.delete"; import "./pages/search-bar"; diff --git a/resources/ts/bo/pages/claims.ts b/resources/ts/bo/pages/claims.ts new file mode 100644 index 0000000..7feb6db --- /dev/null +++ b/resources/ts/bo/pages/claims.ts @@ -0,0 +1,24 @@ +// Type for 'bootstrap modal show' event. Needed for 'relatedTarget'. +interface BootstrapModalShowEvent extends CustomEvent { + relatedTarget: HTMLElement; +} + +document.addEventListener("DOMContentLoaded", function () { + const exportFormModal = document.getElementById("exportModal"); + const claimListInputs = document.getElementsByClassName("claim-list-input"); + + if (!exportFormModal || !claimListInputs) { + return; + } + + exportFormModal.addEventListener("show.bs.modal", (event) => { + const customEvent = event as BootstrapModalShowEvent; + const button = customEvent.relatedTarget as HTMLElement; + const datas = button.getAttribute("data-bs-claim-list"); + + for (const claimListInput of claimListInputs) { + (claimListInput as HTMLInputElement).value = String(datas); + } + }); +}); + diff --git a/resources/ts/bo/pages/search-bar.ts b/resources/ts/bo/pages/search-bar.ts index 4e9685c..a09f225 100644 --- a/resources/ts/bo/pages/search-bar.ts +++ b/resources/ts/bo/pages/search-bar.ts @@ -2,6 +2,7 @@ enum FilterKey { Brand = "brand", Question = "question", Answer = "answer", + Subject = "subject", } function setupFilterListener(selectId: string, paramKey: FilterKey): void { @@ -25,6 +26,7 @@ const filterMap: Record = { brandSelect: FilterKey.Brand, questionSelect: FilterKey.Question, answerSelect: FilterKey.Answer, + subjectSelect: FilterKey.Subject, }; Object.entries(filterMap).forEach(([selectId, paramKey]) => { diff --git a/resources/ts/bo/utilities/fontawesome.ts b/resources/ts/bo/utilities/fontawesome.ts index 324f67a..9dc0c24 100644 --- a/resources/ts/bo/utilities/fontawesome.ts +++ b/resources/ts/bo/utilities/fontawesome.ts @@ -29,6 +29,10 @@ import { faEraser, faEye, faEyeSlash, + faFile, + faFileCsv, + faFileExport, + faFilePdf, faFolderOpen, faListUl, faPaintRoller, @@ -57,6 +61,10 @@ library.add( faTrash, faEye, faEyeSlash, + faFile, + faFileCsv, + faFileExport, + faFilePdf, faFolderOpen, faCrop, faQuestionCircle, diff --git a/resources/views/bo/layouts/sidenav.blade.php b/resources/views/bo/layouts/sidenav.blade.php index 59298d5..050610a 100644 --- a/resources/views/bo/layouts/sidenav.blade.php +++ b/resources/views/bo/layouts/sidenav.blade.php @@ -13,6 +13,20 @@ \App\Models\ClaimSubject::class, \App\Models\ClaimMailTemplate::class, ]; + $defaultFilters = [ + 'App\Models\Customer' => [], + 'App\Models\Brand' => [], + 'App\Models\Question' => [], + 'App\Models\Service' => [], + 'App\Models\SingleOptin' => [], + 'App\Models\Claim' => [ + 'sort_col' => 'created_at', + 'sort_way' => 'desc' + ], + 'App\Models\ClaimSubject' => [], + 'App\Models\ClaimMailTemplate' => [], + + ] @endphp @foreach ($modelsClasses as $crudModel) @can('viewAny', $crudModel) @@ -23,7 +37,10 @@ @if (!$loop->last)
diff --git a/resources/views/bo/modules/claim-tools.blade.php b/resources/views/bo/modules/claim-tools.blade.php new file mode 100644 index 0000000..4c4edfd --- /dev/null +++ b/resources/views/bo/modules/claim-tools.blade.php @@ -0,0 +1,54 @@ +@php + $selectedSubject = request()->subject ?? (isset($modelTable) ? Session::get("crud.$modelTable.subject") : null); +@endphp + +
+
+ +
+
+
+ {{-- SUBJECT SELECT --}} + +
+
+
+ +{{-- EXPORT MODAL --}} + diff --git a/resources/views/bo/modules/search-bar.blade.php b/resources/views/bo/modules/search-bar.blade.php index 7baf593..b63772b 100644 --- a/resources/views/bo/modules/search-bar.blade.php +++ b/resources/views/bo/modules/search-bar.blade.php @@ -67,7 +67,7 @@ @php $parameters = ['rstFltrs' => 'true', 'search' => null, 'page' => null]; if ($routes) { - $parameters = array_merge($parameters, ['brand' => null, 'question' => null, 'answer' => null]); + $parameters = array_merge($parameters, ['brand' => null, 'question' => null, 'answer' => null, 'subject' => null]); } @endphp @can('viewAny', $claimModel) diff --git a/resources/views/bo/pages/claims/index-list.blade.php b/resources/views/bo/pages/claims/index-list.blade.php index b1409f6..fdc7228 100644 --- a/resources/views/bo/pages/claims/index-list.blade.php +++ b/resources/views/bo/pages/claims/index-list.blade.php @@ -2,11 +2,13 @@ @include('bo.modules.table-col-sorter', [ 'cols' => [ - 'reference' => str(__('validation.custom.reference'))->ucfirst(), - 'email' => str(__('models.classes.customers'))->ucfirst(), - 'brand_id' => str(__('models.classes.brands'))->singular()->ucfirst(), - 'type' => str(__('validation.custom.type'))->ucfirst(), - 'status' => str(__('validation.custom.status'))->ucfirst(), + 'reference' => str(__('validation.custom.reference'))->ucfirst(), + 'created_at' => str(__('validation.attributes.created_at'))->ucfirst(), + 'email' => str(__('models.classes.customers'))->ucfirst(), + 'message' => str(__('validation.attributes.message'))->ucfirst(), + 'brand_id' => str(__('models.classes.brands'))->singular()->ucfirst(), + 'type' => str(__('validation.custom.type'))->ucfirst(), + 'status' => str(__('validation.custom.status'))->ucfirst(), 'updated_at' => str(__('validation.attributes.updated_at'))->ucfirst(), ], ]) @@ -16,12 +18,18 @@ @foreach ($claimModels as $claimModel) {{ $claimModel->reference }} + + + {{ $claimModel->created_at->isoFormat('LLLL') }} + +

{{ sprintf('%s %s', $claimModel->first_name, $claimModel->last_name) }}

{{ e($claimModel->email) }}

+ {{ $claimModel->message }} {{ $claimModel->brand->name }} @@ -52,6 +60,16 @@ @endcan + @can('export', $claimModel) + + + + @endcan @else diff --git a/resources/views/bo/pages/claims/index.blade.php b/resources/views/bo/pages/claims/index.blade.php index 56563b1..82ea16e 100644 --- a/resources/views/bo/pages/claims/index.blade.php +++ b/resources/views/bo/pages/claims/index.blade.php @@ -16,6 +16,7 @@
+ @include('bo.modules.claim-tools') @include('bo.modules.search-bar')
diff --git a/resources/views/export/claim_item_pdf.blade.php b/resources/views/export/claim_item_pdf.blade.php new file mode 100644 index 0000000..efcccd1 --- /dev/null +++ b/resources/views/export/claim_item_pdf.blade.php @@ -0,0 +1,21 @@ +

{{ __('Référence') }} : {{ $model->reference }}

+

{{ __('Date de la demande') }} : {{ $model->created_at->toDateString() }}

+

{{ __('Prénom NOM') }} : {{ $model->first_name }} {{ str($model->last_name)->upper() }}

+

{{ __('Email') }} : {{ $model->email }}

+

{{ __('Adresse') }} : {{ sprintf('%s, %s, %s', $model->address, $model->postcode, $model->city) }}

+

{{ __('Statut de la réclamation') }} : {{ $model->status->label() }}

+

{{ __('Sujet de la réclamation') }} : {{ $model->claimSubject->label }}

+

{{ __('Type de demande') }} : {{ $model->type->label() }}

+

{{ __('Marque concernée') }} : {{ $model->brand->name }}

+

{{ __('Message') }} : {{ $model->message }}

+{{-- ATTACHMENT --}} +@if ($model->file_extension) + @if (in_array($model->file_extension, ['jpg', 'jpeg', 'png'])) +

{{ __('Image') }} :

+ + @else +

{{ __('Nom de la pièce jointe') }} : {{ $model->export_file_name }}

+ @endif +@else +

{{ __('Aucune pièce jointe.') }}

+@endif diff --git a/resources/views/export/pdf.blade.php b/resources/views/export/pdf.blade.php new file mode 100644 index 0000000..1b8b7cf --- /dev/null +++ b/resources/views/export/pdf.blade.php @@ -0,0 +1,24 @@ + + + + + + + + + + + @foreach($models as $model) + @include('export.' . $modelName . '_item_pdf', ['model' => $model]) + + {{-- Page break between every claim. --}} + @if (!$loop->last) +
+ @endif + @endforeach + + diff --git a/routes/web-bo.php b/routes/web-bo.php index 4c1d1c0..68cb02b 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -93,6 +93,8 @@ Route::middleware(['throttle:web', 'admin-secure'])->prefix('bo')->name('bo.')-> // * CLAIMS. Route::resource('claims', ClaimController::class)->only(['index', 'edit', 'update']); Route::post('claims/{claim}/answer', [ClaimController::class, 'answer'])->name('claims.answer'); + Route::post('claims/export', [ClaimController::class, 'export']) + ->name('claims.export'); // * CLAIM SUBJECTS. Route::get('/{brand}/claim_subjects/json/paginate', [ClaimController::class, 'jsonClaimPaginateModels'])