Merge branch '49-update-recla' into 'develop'

feat: many ui improvements for the claim part

See merge request ldc-web/projets/crm!46
This commit is contained in:
noah.lancien 2025-12-02 10:40:31 +00:00
commit b6fe43cdcf
29 changed files with 1447 additions and 13 deletions

View file

@ -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',
];
/**

View file

@ -0,0 +1,85 @@
<?php
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ExportFormat: int
{
use BaseEnum;
case csv = 1;
case csvWithoutAttachment = 2;
case pdf = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::csv->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',
};
}
}

View file

@ -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()) {

View file

@ -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<int, \App\Models\Claim> $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
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Http\Requests\Bo\Claims;
use App\Enums\ExportFormat;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ExportClaimRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->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<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|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'),
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Interfaces;
interface IsExportable
{
/**
* Get the formatted csv line for the current model.
*
* @return array
*/
public function getFormattedCsvLine(): array;
/**
* Get the csv headers for this model.
*
* @return array
*/
public static function getCsvHeaders(): array;
/**
* 'file_extension' virtual property.
*
* @return string
*/
public function getFileExtensionAttribute(): string;
/**
* 'export_file_name' virtual property.
*
* @return string
*/
public function getExportFileNameAttribute(): string;
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Lib\Helpers;
class FileHelper
{
/**
* Get file path in private storage from stored path.
*
* @param string $basePath
* @return string
*/
public static function getPrivateFilePath(string $basePath): string
{
return str($basePath)->replaceFirst('storage/app/private', '');
}
}

View file

@ -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');
}
}

View file

@ -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.
*

View file

@ -0,0 +1,229 @@
<?php
namespace App\Traits\Controllers;
use App\Enums\ExportFormat;
use App\Lib\Helpers\FileHelper;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ZipArchive;
trait ExportDatas
{
/**
* Check if the export needs to be in a .zip file.
* .zip is needed for :
* - csv with any files
* - pdf with files other than pictures
*
* @param ExportFormat $format Requested format.
* @param \Illuminate\Database\Eloquent\Collection $models Collection of models to export.
* @param string $fileFieldName Name of the field containing the file.
* @return boolean
*/
public function needsZip(
ExportFormat $format,
\Illuminate\Database\Eloquent\Collection $models,
string $fileFieldName = 'file'
): bool {
if ($format->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()
]
);
}
}

View file

@ -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",

367
composer.lock generated
View file

@ -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",

304
config/dompdf.php Normal file
View file

@ -0,0 +1,304 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
// Throw an Exception on warnings from dompdf.
'show_warnings' => 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, <img> 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 <script type="text/php"> ... </script> 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 <script type="text/javascript"> ... </script> 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,
],
];

View file

@ -67,7 +67,6 @@ return [
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
/*

View file

@ -10,6 +10,7 @@ return [
'delete' => 'delete',
'save' => 'save',
'edit' => 'edit',
'export' => 'export',
],
'helpers' => [

View file

@ -10,6 +10,7 @@ return [
'delete' => 'supprimer',
'save' => 'sauvegarder',
'edit' => 'mettre à jour',
'export' => 'exporter',
],
'helpers' => [

View file

@ -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";

View file

@ -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);
}
});
});

View file

@ -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<string, FilterKey> = {
brandSelect: FilterKey.Brand,
questionSelect: FilterKey.Question,
answerSelect: FilterKey.Answer,
subjectSelect: FilterKey.Subject,
};
Object.entries(filterMap).forEach(([selectId, paramKey]) => {

View file

@ -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,

View file

@ -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 @@
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link w-fit" data-bs-tooltip="tooltip" data-bs-placement="right"
href="{{ route(sprintf('%s%s.index', $prefix ?? '', $crudModel->getTable())) }}"
href="{{ route(
sprintf('%s%s.index', $prefix ?? '', $crudModel->getTable()),
$defaultFilters[get_class($crudModel)]
) }}"
title="{{ __('Afficher la liste') }}">
{{ __('Liste') }}
</a>
@ -37,6 +54,18 @@
</a>
</li>
@endcan
@if ($crudModel instanceof \App\Models\Claim)
<li class="nav-item">
<a class="nav-link w-fit" data-bs-tooltip="tooltip" data-bs-placement="right"
href="{{ route(
sprintf('%s%s.index', $prefix ?? '', $crudModel->getTable()),
['status' => [\App\Enums\Claims\ClaimStatus::treated, \App\Enums\Claims\ClaimStatus::refused]]
) }}"
title="{{ __('Afficher l\'historique') }}">
{{ __('Historique') }}
</a>
</li>
@endif
</ul>
@if (!$loop->last)
<hr>

View file

@ -0,0 +1,54 @@
@php
$selectedSubject = request()->subject ?? (isset($modelTable) ? Session::get("crud.$modelTable.subject") : null);
@endphp
<div class="row align-items-center mb-3">
<div class="col-1">
<button class="btn btn-primary d-flex flex-column" id="export-btn" data-bs-toggle="modal" data-bs-target="#exportModal" data-bs-claim-list="@json($claimModels->pluck('id')->toArray())">
<i class="fa-solid fa-file-export"></i>
{{ __('Exporter')}}
</button>
</div>
<div class="col-3 offset-8">
<form action="{{ request()->fullUrlWithQuery(['rstFltrs' => null]) }}" class="row gx-2 mb-2">
{{-- SUBJECT SELECT --}}
<select class="form-select mb-1 mb-lg-0"
id="subjectSelect" role="button" name="subject">
<option value="0" @selected(!$selectedSubject)>{{ __('Sélectionnez un sujet...') }}</option>
@foreach ($claimSubjectModels as $claimSubjectModel)
<option value="{{ $claimSubjectModel->getKey() }}" @selected($selectedSubject == $claimSubjectModel->getKey())>
{{ $claimSubjectModel->label }}
</option>
@endforeach
</select>
</form>
</div>
</div>
{{-- EXPORT MODAL --}}
<div class="modal fade" id="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="exportModalLabel">{{ __('Exporter les réclamations')}}</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
@foreach(\App\Enums\ExportFormat::cases() as $supportedFormat)
<form action="{{ route('bo.claims.export', ['format' => $supportedFormat->name]) }}" class="row gx-2 mb-2" method="POST">
@csrf
<input hidden name="format" value="{{ $supportedFormat }}">
<input hidden class="claim-list-input" name="claimList" value="">
<button class="btn btn-{{ $supportedFormat->class()}} d-flex flex-column" id="export-{{ $supportedFormat->name }}" type="submit">
<i class="{{ $supportedFormat->picto() }}"></i>
{{ __('Exporter en .:format', ['format' => $supportedFormat->label()])}}
</button>
</form>
@endforeach
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ __('Fermer')}}</button>
</div>
</div>
</div>
</div>

View file

@ -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
<a class="btn btn-danger ms-2" data-bs-tooltip="tooltip"

View file

@ -11,7 +11,7 @@
<div class="d-flex align-items-start flex-row">
@can('viewAny', $claimModel)
<a class="btn btn-primary text-decoration-none me-2" data-bs-tooltip="tooltip" data-bs-placement="top"
data-bs-tooltip="tooltip" href="{{ route('bo.claims.index') }}"
data-bs-tooltip="tooltip" href="{{ route('bo.claims.index', ['sort_col' => 'created_at', 'sort_way' => 'desc']) }}"
title="{{ __('crud.helpers.list_all', ['model' => __('models.classes.claims')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>

View file

@ -2,11 +2,13 @@
<thead class="border-top-0 border-start-0 border-end-0 border border-2">
@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)
<tr class="border-bottom list-item">
<td class="text-center align-middle">{{ $claimModel->reference }}</td>
<td class="text-center align-middle">
<span class="badge bg-secondary text-bg-secondary">
{{ $claimModel->created_at->isoFormat('LLLL') }}
</span>
</td>
<td class="text-center align-middle">
<div class="d-flex flex-column justify-content-center align-items-center">
<p class="m-0">{{ sprintf('%s %s', $claimModel->first_name, $claimModel->last_name) }}</p>
<p class="text-muted m-0">{{ e($claimModel->email) }}</p>
</div>
</td>
<td class="text-center align-middle">{{ $claimModel->message }}</td>
<td class="text-center align-middle">{{ $claimModel->brand->name }}</td>
<td class="text-center align-middle">
<x-bo.claims.enum-badge :claimModel="$claimModel" enum="type" />
@ -52,6 +60,16 @@
<i class="fa-solid fa-comments"></i>
</a>
@endcan
@can('export', $claimModel)
<a class="btn btn-secondary d-flex flex-column"
data-bs-tooltip="tooltip"
title="{{ str(__('crud.actions.export'))->ucfirst() }}"
data-bs-toggle="modal"
data-bs-target="#exportModal"
data-bs-claim-list="@json([$claimModel->id])">
<i class="fa-solid fa-file-export"></i>
</a>
@endcan
</div>
@else
<span class="text-danger" data-bs-tooltip="tooltip" title="{{ __('list.right') }}">

View file

@ -16,6 +16,7 @@
</div>
</div>
<div class="card-body">
@include('bo.modules.claim-tools')
@include('bo.modules.search-bar')
<div class="row w-100 mx-auto mb-2">
<div class="col table-responsive p-0">

View file

@ -0,0 +1,21 @@
<p>{{ __('Référence') }}&nbsp;: {{ $model->reference }} </p>
<p>{{ __('Date de la demande') }}&nbsp;: {{ $model->created_at->toDateString() }} </p>
<p>{{ __('Prénom NOM') }}&nbsp;: {{ $model->first_name }} {{ str($model->last_name)->upper() }}</p>
<p>{{ __('Email') }}&nbsp;: {{ $model->email }} </p>
<p>{{ __('Adresse') }}&nbsp;: {{ sprintf('%s, %s, %s', $model->address, $model->postcode, $model->city) }} </p>
<p>{{ __('Statut de la réclamation') }}&nbsp;: {{ $model->status->label() }} </p>
<p>{{ __('Sujet de la réclamation') }}&nbsp;: {{ $model->claimSubject->label }} </p>
<p>{{ __('Type de demande') }}&nbsp;: {{ $model->type->label() }} </p>
<p>{{ __('Marque concernée') }}&nbsp;: {{ $model->brand->name }} </p>
<p>{{ __('Message') }}&nbsp;: {{ $model->message }} </p>
{{-- ATTACHMENT --}}
@if ($model->file_extension)
@if (in_array($model->file_extension, ['jpg', 'jpeg', 'png']))
<p>{{ __('Image') }}&nbsp;:</p>
<img src="{{ $model->fileBase64 }}">
@else
<p>{{ __('Nom de la pièce jointe') }}&nbsp;: {{ $model->export_file_name }}</p>
@endif
@else
<p>{{ __('Aucune pièce jointe.') }}</p>
@endif

View file

@ -0,0 +1,24 @@
<!doctype html>
<html dir="ltr" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
.page-break {
page-break-after: always;
}
</style>
</head>
<body>
@foreach($models as $model)
@include('export.' . $modelName . '_item_pdf', ['model' => $model])
{{-- Page break between every claim. --}}
@if (!$loop->last)
<div class="page-break"></div>
@endif
@endforeach
</body>
</html>

View file

@ -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'])