authorizeResource(Claim::class); } /** * Display a listing of the resource. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Contracts\View\View */ public function index(Request $request): \Illuminate\Contracts\View\View { /** @var \App\Models\User $userModel */ $userModel = auth('backend')->user(); /** @var \Illuminate\Database\Eloquent\Builder $query */ $query = Claim::query() ->when( $request->filled('brand'), fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand)) )->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 */ $search = $request->search; if ($search) { $this->searchQuery( $query, $search, null, 'reference', 'first_name', 'last_name', 'email', ); } $searchFields = \implode(', ', [ trans('validation.custom.reference'), trans('validation.attributes.first_name'), trans('validation.attributes.last_name'), trans('validation.attributes.email'), ]); /** Sort columns with a query */ $this->sortQuery($query); /** Custom pagination */ $claimModels = $query->simplePaginate($this->getPagination()); $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')); } /** * Show the form for editing the specified resource. * * @param App\Models\Claim $claim * @return \Illuminate\Contracts\View\View */ public function edit(Claim $claim): \Illuminate\Contracts\View\View { $customerClaimModels = !is_null($claim->customer) ? Claim::query() ->whereNotNull('customer_id') ->whereHas('brand', fn($q) => $q->whereKey($claim->brand->getKey())) ->whereHas('customer', fn($q) => $q->whereKey($claim->customer->getKey())) ->whereNot('id', $claim->getKey()) ->whereNot('status', ClaimStatus::treated)->paginate($this->jsonItemPerPagination) : collect(); $brandModels = CollectionHelper::paginate( Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination, ); $claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey()); $claimSubjectModels = CollectionHelper::paginate( ClaimSubject::getAllModelsFromBrand($claim->brand->getKey()), $this->jsonItemPerPagination, ); $claimTemplateModels = CollectionHelper::paginate( ClaimMailTemplate::getAllModelsFromBrand($claim->brand->getKey()), $this->jsonItemPerPagination, ); $customerModel = CustomerInfo::query()->where('hashcode', $claim->hashcode)->first(); return view('bo.pages.claims.edit', [ 'claimModel' => $claim->load(['brand', 'claimSubject']), 'customerClaimModels' => $customerClaimModels, 'brandModels' => $brandModels, 'claimMessageModels' => $claimMessageModels, 'claimSubjectModels' => $claimSubjectModels, 'claimTemplateModels' => $claimTemplateModels, 'customerModel' => $customerModel, ]); } /** * Update the specified resource. * * @param \App\Http\Requests\Bo\Claims\UpdateClaimRequest $request * @param \App\Models\Claim $claim * @return \Illuminate\Http\JsonResponse */ public function update(UpdateClaimRequest $request, Claim $claim): \Illuminate\Http\JsonResponse { return DB::transaction(function () use ($request, $claim) { $claim->fill($request->validated()); if ($claim->saveOrFail()) { $claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey()); return response()->json($claimMessageModels); } return response()->json([], 500); }); } /** * Send the claim answer. * * @param \App\Http\Requests\Bo\Claims\SendClaimAnswerRequest $request * @param \App\Models\Claim $claim * @return \Illuminate\Http\JsonResponse */ public function answer(SendClaimAnswerRequest $request, Claim $claim): \Illuminate\Http\JsonResponse { return DB::transaction(function () use ($request, $claim) { /** @var \App\Models\User $userModel */ $userModel = auth()->user(); $enum = ClaimResponseType::manual; $claim->response_type = $enum; $claim->status = ClaimStatus::treated; if ($claim->saveOrFail()) { // Send the response to the claim author. Mail::to($claim->email)->send(new ClaimAnswerMail($claim, $request->validated()['content'])); // Create custom event with content message. // @phpcs:disable Generic.Files.LineLength.TooLong $content = sprintf( "%s %s %s %s
%s", $userModel->first_name, $userModel->last_name, trans('a modifié :field en', ['field' => trans('validation.custom.response_type')]), $claim->response_type->label(), clean($request->validated()['content']), ); event(new ClaimModelChanged($claim, '', '', null, $content)); // @phpcs:enable // Return conversation. $claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey()); return response()->json($claimMessageModels); } //end if return response()->json([], 500); }); } /** * Get a paginate resource. * Needed to overheight the controller method and add a brand condition. * * @param \App\Http\Requests\Bo\PaginateModelsRequest $request * @return \Illuminate\Http\JsonResponse */ public function jsonClaimPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse { $data = $request->validated(); $brandModelKey = $request->route()->parameter('brand')->getKey(); $models = CollectionHelper::paginateManually( $data['targetModel']::getAllModelsFromBrand($brandModelKey), $this->jsonItemPerPagination, $request->validated('page'), ); 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 = ClaimExportFormat::from($request->validated('format')); // * CSV if ($format->equals(ClaimExportFormat::csvWithoutAttachment, ClaimExportFormat::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->returnStreamedZip($format, $claims, $csv); // * PDF } elseif ($format->equals(ClaimExportFormat::pdf)) { $pdf = $this->generatePdf($claims); if (!$this->needsZip(ClaimExportFormat::pdf, $claims)) { return $pdf->download('export.pdf'); } return $this->returnStreamedZip($format, $claims, $pdf->output()); } else { return back()->with( 'error', __('Une erreur est survenue lors de l\'export. Veuillez réessayer en rafraîchissant la page.') ); }; //end if } /** * Check if the export needs to be in a .zip file. * * @param ClaimExportFormat $format * @param \Illuminate\Database\Eloquent\Collection $claims * @return boolean */ private function needsZip(ClaimExportFormat $format, \Illuminate\Database\Eloquent\Collection $claims): bool { /** * .zip is needed for : * - csv with any files * - pdf with files other than pictures */ if ($format->equals(ClaimExportFormat::csv)) { return $claims->whereNotNull('file')->pluck('file')->isNotEmpty(); } elseif ($format->equals(ClaimExportFormat::pdf)) { return $claims ->whereNotNull('file') ->whereNotIn('file_extension', ['jpg', 'jpeg', 'png']) ->isNotEmpty(); } return false; } /** * Create the zip folder for the export with the main file inside. * * @param ZipArchive $zip * @param string $file * @param ClaimExportFormat $format * @return string|false * * @throws \Exception If the zip opening failed. */ private function createExportZip(ZipArchive &$zip, string $file, ClaimExportFormat $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 $claims * @param ClaimExportFormat $format * @return void */ private function addAttachmentsToZip( ZipArchive &$zip, \Illuminate\Database\Eloquent\Collection $claims, ClaimExportFormat $format ) { $claimsWithFilesToZip = $claims->whereNotNull('file') ->when($format->equals(ClaimExportFormat::pdf), function ($collection) { return $collection->whereNotIn('file_extension', ['jpg', 'jpeg', 'png']); }); foreach ($claimsWithFilesToZip as $claim) { $filePath = $this->getPrivateFilePath($claim->file); if (Storage::disk('private')->exists($filePath)) { $zip->addFile( Storage::disk('private')->path($filePath), $claim->export_file_name ); } } } /** * Return a StreamedResponse with a zip file. * * @param ClaimExportFormat $format * @param \Illuminate\Database\Eloquent\Collection $claims * @param string $file * @return \Symfony\Component\HttpFoundation\StreamedResponse */ private function returnStreamedZip( ClaimExportFormat $format, \Illuminate\Database\Eloquent\Collection $claims, string $file ): \Symfony\Component\HttpFoundation\StreamedResponse { return new StreamedResponse(function () use ($file, $claims, $format) { // Create zip. $zip = new ZipArchive(); $zipName = $this->createExportZip($zip, $file, $format); // Add attachments. $this->addAttachmentsToZip($zip, $claims, $format); $zip->close(); readfile($zipName); unlink($zipName); }, 200, $this->getFileHeaders($format, true)); } /** * Generate PDF with claims. * * @param \Illuminate\Database\Eloquent\Collection $claims * @return \Barryvdh\DomPDF\PDF */ private function generatePdf(\Illuminate\Database\Eloquent\Collection $claims): \Barryvdh\DomPDF\PDF { // Convert pictures to base64. foreach ($claims->whereIn('file_extension', ['jpg', 'jpeg', 'png']) as $claim) { $filePath = $this->getPrivateFilePath($claim->file); $claim->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'); } return Pdf::setOptions(['tempDir' => Storage::disk('private')->path('temp')]) ->loadView('bo.pages.claims.pdf', [ 'claimModels' => $claims ]); } /** * Generate claims CSV * * @param \Illuminate\Database\Eloquent\Collection $claims * @return string|false */ private function generateCsv(\Illuminate\Database\Eloquent\Collection $claims) { // CSV Header. $csvlines[] = [ 'Référence', 'Date de création', 'Prénom NOM', 'Email', 'Adresse', 'Statut', 'Sujet', 'Type', 'Marque', 'Message', 'Pièce jointe', ]; foreach ($claims as $claim) { $csvlines[] = $this->fillLine($claim); } return $this->array2csv($csvlines, ';'); } /** * 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; } /** * Fill one line for csv. * * @param \App\Models\Claim $claim * @return array */ private function fillLine(Claim $claim) { $oneline = [ $claim->reference, $claim->created_at->toDateTimeString(), sprintf('%s %s', $claim->first_name, Str::upper($claim->last_name)), $claim->email, sprintf('%s, %s, %s', $claim->address, $claim->postcode, $claim->city), $claim->status->label(), $claim->claimSubject->label, $claim->type->label(), $claim->brand->name, $claim->message, $claim->export_file_name, ]; return $oneline; } /** * Get file path in private storage from stored path. * * @param string $basePath * @return string */ private function getPrivateFilePath(string $basePath): string { return str($basePath)->replaceFirst('storage/app/private', ''); } /** * Get file headers to use with StreamedResponse. * * @param ClaimExportFormat $format * @param boolean $isZip * @return array */ private function getFileHeaders(ClaimExportFormat $format, bool $isZip = false): array { if ($isZip) { return [ 'Content-Type' => 'application/zip', 'Content-Disposition' => 'attachment; filename="export.zip"', ]; }; return $format->fileHeaders(); } }