authorizeResource(Customer::class); } /** * View all customers. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Contracts\View\View */ public function index(Request $request): \Illuminate\Contracts\View\View { $modelClass = Customer::class; $modelTable = (new Customer())->getTable(); $questionModels = null; $answerModels = null; $searchFields = trans('Email, Prénom, Nom'); // Reset session sorts. if ($request->rstSrts) { $this->resetSessionSorts($modelTable); } // Reset session filters. if ($request->rstFltrs) { $this->resetSessionFilters($modelTable); } if ($request->filled('search') || Session::get("crud.$modelTable.search")) { $columns = ['customer_infos.lastname', 'customer_infos.firstname']; $query = $this->buildCustomerQuery(); // Apply filters. $this->applyBrandFilter($request, $query, $modelTable, $questionModels, $answerModels); $this->applySearchFilter($request, $query, $modelTable, $columns); $this->applySort($request, $query, $modelTable); $customerModels = $query->with(['customer', 'brand'])->simplePaginate($this->getPagination()); } //end if return view('bo.pages.customers.index', [ 'modelClass' => $modelClass, 'modelTable' => $modelTable, 'customerModels' => $customerModels ?? collect(), 'searchFields' => $searchFields, 'questionModels' => $questionModels, 'answerModels' => $answerModels, ]); } /** * Show a specific custmer. * * @param \App\Models\Customer $customer * @param \App\Models\Brand $brand * @return \Illuminate\Contracts\View\View */ public function show(Customer $customer, Brand $brand): \Illuminate\Contracts\View\View { $customerInfos = CustomerInfo::query()->where('customer_id', $customer->id) ->where('brand_id', $brand->id)->firstOrFail(); $customerLogin = CustomerLogin::query()->where('customer_id', $customer->id) ->where('brand_id', $brand->id)->first(); return view('bo.pages.customers.show', compact( 'customerInfos', 'customerLogin' )); } /** * Delete a specific customer info. * * @param \App\Models\Customer $customer * @param \App\Models\Brand $brand * @return \Illuminate\Http\RedirectResponse */ public function delete(Customer $customer, Brand $brand): \Illuminate\Http\RedirectResponse { try { /** @var \App\Models\CustomerInfo */ $customerInfo = $customer->info($brand, Customer::FIRST_OR_FAIL); /** @var \App\Models\CustomerLogin */ $customerLogin = $customer->login($brand, Customer::FIRST_OR_FAIL); DB::transaction(function () use ( $brand, $customer, $customerLogin, $customerInfo ) { if ( $customerInfo->delete() === false || $customerLogin->delete() === false ) { throw new \ErrorException('Api failed to delete customer.'); }; $customer->optInHistory()->whereHas('brand', function ($q) use ($brand) { $q->where('id', $brand->id); })->delete(); $logins = $customer->logins()->count(); $infos = $customer->infos()->count(); $history = $customer->optInHistory()->count(); // If customer is not associated with any brand then delete. if (!$logins and !$infos and !$history) { if ($customer->delete() === false) { throw new \ErrorException('Api failed to update customer.'); } } }); } catch (\ErrorException $e) { return redirect()->route('bo.customers.index') ->with('error', trans('Le compte n\'a pas pu être supprimé.')); } //end try return redirect()->route('bo.customers.index')->with('warning', trans('Compte supprimé.')); } /** * Show export the customers form. * * @return \Illuminate\Contracts\View\View */ public function export(): \Illuminate\Contracts\View\View { $this->authorize('export', Customer::class); $brands = Brand::all(); return view('bo.pages.customers.export', compact( 'brands' )); } /** * Exports customers following the requested export type. * * @param \App\Http\Requests\Bo\CustomerExport $request * @return \Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse * @throws \Exception If the request export type in not handled. */ public function exportSubmit( CustomerExport $request ): \Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse { $pagination = 2000; $w = $this->buildCsvWriter($request->action); $exportType = CustomerExportType::make(\intval($request->export_type)); $response = $this->buildStreamedResponse($exportType); switch ($exportType) { case CustomerExportType::both: $continue = true; // ! Continue case CustomerExportType::customer: $page = 1; /** @var \Illuminate\Database\Eloquent\Builder $req */ $req = CustomerInfo::query()->where('brand_id', $request->brand) ->where('opt_in', true); $total = $req->count(); $pages = \ceil($total / $pagination); do { /** @var \Illuminate\Pagination\LengthAwarePaginator $items */ $items = $req->with('customer', function ($q) { $q->select(['id', 'email']); })->paginate( $pagination, ['lastname', 'firstname', 'hashcode', 'customer_id'], 'page', $page ); $items->map(function (CustomerInfo $customerInfo) use ($w) { $w->insertOne([ 'lastname' => $customerInfo->lastname, 'firstname' => $customerInfo->firstname, 'email' => $customerInfo->customer->email, 'hashcode' => $customerInfo->hashcode ]); }); } while ($page++ <= $pages); if (!isset($continue)) { break; } // ! Continue case CustomerExportType::singleOptin: SingleOptin::query()->whereHas('brand', function (Builder $q) use ($request) { $q->where('id', $request->brand); })->get()->map(function (SingleOptin $singleOptin) use ($w) { $w->insertOne([ 'lastname' => '', 'firstname' => '', 'email' => $singleOptin->email, 'hashcode' => $singleOptin->hashcode ]); }); break; default: throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label())); } //end switch if ($request->action === "export") { return $this->buildCsvDownloadResponse($response, $w); } return $this->importToMailjet($request, $w) } /** * Reset all session sorts. * * @param string $table * @return void */ private function resetSessionSorts(string $table): void { Session::forget([ "crud.$table.sort_col", "crud.$table.sort_way", ]); } /** * Reset all session filters. * * @param string $table * @return void */ private function resetSessionFilters(string $table): void { Session::forget([ "crud.$table.brand", "crud.$table.question", "crud.$table.answer", "crud.$table.search", ]); } /** * Build the customer query. * * @return Builder */ private function buildCustomerQuery(): Builder { return CustomerInfo::query() ->join('customer_logins', function (JoinClause $join) { $join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id') ->on('customer_logins.customer_id', '=', 'customer_infos.customer_id'); }) ->join('customers', fn($join) => $join->on('customers.id', '=', 'customer_infos.customer_id')) ->join('brands', fn($join) => $join->on('brands.id', '=', 'customer_infos.brand_id')); } /** * Build the streamed HTTP response. * * @param CustomerExportType $exportType * @return StreamedResponse */ private function buildStreamedResponse(CustomerExportType $exportType): StreamedResponse { // * We send the CSV using Symfony StreamedResponse. $response = new StreamedResponse(); $response->headers->set('Content-Encoding', 'none'); $response->headers->set('Content-Type', 'text/csv; charset=UTF-8'); $response->headers->set('Description', 'File Transfer'); $response->headers->set( 'Content-Disposition', sprintf( 'attachment; filename="Export pour newsletter %s %s.csv"', Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'), Str::slug($exportType->label()) ) ); return $response; } /** * Build the CSV writer. * * @param string $action * @return Writer */ private function buildCsvWriter(string $action): Writer { if ($action === 'export') { $header = '"lastname";"firstname";"email";"hashcode"'; $w = Writer::createFromString($header . PHP_EOL); $w->setDelimiter(';'); } else { $header = 'lastname,firstname,email,hashcode'; $w = Writer::createFromString($header . PHP_EOL); $w->setDelimiter(','); } $w->setEnclosure('"'); $w->setOutputBOM(Writer::BOM_UTF8); return $w; } /** * Attach a download callback to the response. * * @param StreamedResponse $response * @param Writer $writer * @return StreamedResponse */ private function buildCsvDownloadResponse(StreamedResponse $response, Writer $writer): StreamedResponse { /* We create a callable to output the CSV in chunk with Symfony StreamResponse you can flush the body content if necessary see Symfony documentation for more information */ // * The flush value should depend on your CSV size. $flush_threshold = 1000; $response->setCallback(function () use ($writer, $flush_threshold) { foreach ($writer->chunk(1024) as $offset => $chunk) { echo $chunk; if ($offset % $flush_threshold === 0) { flush(); } } }); return $response; } /** * Import contacts to Mailjet * * @param CustomerExport $request * @param Writer $writer * @return \Illuminate\Http\RedirectResponse */ private function importToMailjet(CustomerExport $request, Writer $writer): \Illuminate\Http\RedirectResponse { $brandModel = Brand::find($request->brand); $contactList = MailjetHelper::createContactList( sprintf("%s - %s", now()->format('d/m/Y'), $brandModel->name) ); if (empty($contactList)) { return back()->withErrors('Impossible de créer la liste de contacts Mailjet.'); } $mailjetListId = $contactList[0]['ID']; BrandContactList::create([ 'brand_id' => $brandModel->getKey(), 'mailjet_list_id' => $mailjetListId, ]); $csvData = MailjetHelper::uploadCsvFile($mailjetListId, $writer->toString()); $importJob = MailjetHelper::importContacts($mailjetListId, $csvData['ID']); return back()->with('success', 'Liste créée et contacts importés avec succès.'); } /** * Apply all filters (brand, question, answer). * * @param \Illuminate\Http\Request $request * @param \Illuminate\Database\Eloquent\Builder $query * @param string $table * @param \Illuminate\Support\Collection|null $questionModels * @param \Illuminate\Support\Collection|null $answerModels * @return void */ private function applyBrandFilter( Request $request, Builder &$query, string $table, ?Collection &$questionModels, ?Collection &$answerModels ): void { $brand = $request->brand ?? Session::get("crud.$table.brand"); if (!(int) $brand) { return; } Session::put("crud.$table.brand", $brand); $query->where('customer_infos.brand_id', $brand); $questionModels = Question::getAllModelsFromBrand($brand); if (($request->filled('question') || Session::has("crud.$table.question")) && $questionModels->count()) { $question = $request->question ?? Session::get("crud.$table.question"); Session::put("crud.$table.question", $question); $answerModels = Answer::getAllAnswersFromQuestion($question); $questionAnswers = Question::find($question)?->answers->modelKeys() ?? []; $query->whereHas('customer.answers', fn($q) => $q->whereIn('answer_id', $questionAnswers)); if ( ($request->filled('answer') || Session::has("crud.$table.answer")) && $questionModels->count() && $answerModels->count() ) { $answer = $request->answer ?? Session::get("crud.$table.answer"); Session::put("crud.$table.answer", $answer); $query->whereHas('customer.answers', fn($q) => $q->whereKey($answer)); } } } /** * Apply the search filter. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Database\Eloquent\Builder $query * @param string $table * @param array $columns * @return void */ private function applySearchFilter(Request $request, Builder &$query, string $table, array $columns): void { $search = $request->search ?? Session::get("crud.$table.search"); Session::put("crud.$table.search", $search); $query->where(function ($q) use ($columns, $search) { foreach ($columns as $column) { $q->orWhere($column, 'LIKE', "%$search%"); } $q->orWhere('customers.email', 'LIKE', "%$search%") ->orWhere('customer_logins.last_connection', 'LIKE', "%$search%"); }); } /** * Apply sorts. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Database\Eloquent\Builder $query * @param string $table * @return void */ private function applySort(Request $request, Builder &$query, string $table): void { $sortCol = $request->sort_col ?? Session::get("crud.$table.sort_col"); $sortWay = $request->sort_way ?? Session::get("crud.$table.sort_way"); if ($sortCol) { Session::put("crud.$table.sort_col", $sortCol); } if ($sortWay) { Session::put("crud.$table.sort_way", $sortWay); } $sortable = [ 'brands.name', 'customers.email', 'customer_infos.firstname', 'customer_infos.lastname', 'customer_infos.opt_in', 'customer_logins.last_connection', 'customer_logins.status', ]; if ($sortCol && $sortWay && (Schema::hasColumn($table, $sortCol) || in_array($sortCol, $sortable))) { $query->orderBy($sortCol, $sortWay); } } }