middleware('admin'); } public function index(NewsletterListFilterRequest $request): View|RedirectResponse { if ($request->get('reset') === 'filter') { set_user_attr(self::FILTER_SETTING_KEY, null); return redirect(route('admin_newsletter_lists')); } $filter = $request->query() === [] ? NewsletterListFilter::fromArray((array) (get_user_attr(self::FILTER_SETTING_KEY) ?: [])) : $request->toFilter(); return $this->renderIndex($filter, $request->listId() ? NewsletterList::find($request->listId()) : null); } /** * Lädt eine gespeicherte Liste in die Filter-Seite. */ public function load(NewsletterList $list): View { set_user_attr(self::FILTER_SETTING_KEY, $list->toFilter()->toArray()); return $this->renderIndex($list->toFilter(), $list); } public function datatable(NewsletterListFilterRequest $request): JsonResponse { $filter = $request->toFilter(); set_user_attr(self::FILTER_SETTING_KEY, $filter->toArray()); $result = $this->service->cached($filter, (int) $request->user()->id, $request->boolean('refresh')); return DataTables::of($result->rows) ->editColumn('email', fn (array $row) => e($row['email'])) ->addColumn('name', fn (array $row) => e(trim($row['firstname'].' '.$row['lastname']))) ->editColumn('optin_label', fn (array $row) => $this->optinBadge($row)) ->editColumn('status_label', fn (array $row) => $this->statusBadge($row)) ->rawColumns(['optin_label', 'status_label']) ->with('stats', $result->stats) ->toJson(); } public function export(NewsletterListFilterRequest $request, string $format): BinaryFileResponse|StreamedResponse { $filter = $request->toFilter(); set_user_attr(self::FILTER_SETTING_KEY, $filter->toArray()); $list = $request->listId() ? NewsletterList::find($request->listId()) : null; return $this->download($request->user(), $filter, $format, $list); } /** * Export direkt aus der Übersicht der gespeicherten Listen mit den dort hinterlegten Filtern. */ public function exportSaved(Request $request, NewsletterList $list, string $format): BinaryFileResponse|StreamedResponse { return $this->download($request->user(), $list->toFilter(), $format, $list); } public function store(StoreNewsletterListRequest $request): RedirectResponse { $list = new NewsletterList([ 'name' => $request->listName(), 'created_by' => $request->user()->id, ]); $list->applyFilter($request->toFilter()); $list->save(); return redirect(route('admin_newsletter_lists_load', [$list->id]))->with('alert-success', __('newsletter.saved.stored')); } public function update(StoreNewsletterListRequest $request, NewsletterList $list): RedirectResponse { $list->name = $request->listName(); $list->applyFilter($request->toFilter()); $list->save(); return redirect(route('admin_newsletter_lists_load', [$list->id]))->with('alert-success', __('newsletter.saved.updated')); } public function destroy(NewsletterList $list): RedirectResponse { $list->exports()->update(['newsletter_list_id' => null]); $list->delete(); return redirect(route('admin_newsletter_lists_saved'))->with('alert-success', __('newsletter.saved.deleted')); } public function saved(): View { return view('admin.newsletter.saved'); } public function savedDatatable(): JsonResponse { $query = NewsletterList::query()->with('creator.account')->select('newsletter_lists.*'); return DataTables::eloquent($query) ->editColumn('name', fn (NewsletterList $list) => ''.e($list->name).'') ->editColumn('source', fn (NewsletterList $list) => __('newsletter.source_option.'.$list->source)) ->addColumn('summary', fn (NewsletterList $list) => e($this->service->describe($list->toFilter()))) ->editColumn('last_exported_at', fn (NewsletterList $list) => $list->last_exported_at?->format('d.m.Y H:i') ?? '–') ->editColumn('last_row_count', fn (NewsletterList $list) => $list->last_row_count === null ? '–' : (string) $list->last_row_count) ->addColumn('creator', fn (NewsletterList $list) => e($list->creator?->getFullName(false) ?? '')) ->editColumn('created_at', fn (NewsletterList $list) => $list->created_at?->format('d.m.Y') ?? '') ->addColumn('actions', fn (NewsletterList $list) => $this->savedActions($list)) ->orderColumn('name', 'name $1') ->orderColumn('last_exported_at', 'last_exported_at $1') ->orderColumn('last_row_count', 'last_row_count $1') ->orderColumn('created_at', 'created_at $1') ->rawColumns(['name', 'actions']) ->make(true); } public function exports(): View { return view('admin.newsletter.exports'); } public function exportsDatatable(): JsonResponse { $query = NewsletterListExport::query()->with(['list', 'user.account'])->select('newsletter_list_exports.*'); return DataTables::eloquent($query) ->editColumn('created_at', fn (NewsletterListExport $export) => $export->created_at?->format('d.m.Y H:i') ?? '') ->addColumn('user', fn (NewsletterListExport $export) => e($export->user?->getFullName(false) ?? '')) ->addColumn('list', function (NewsletterListExport $export) { if ($export->list) { return ''.e($export->list->name).''; } return ''.e(__('newsletter.exports.adhoc')).''; }) ->addColumn('summary', fn (NewsletterListExport $export) => e($this->service->describe(NewsletterListFilter::fromArray((array) $export->filters)))) ->editColumn('format', fn (NewsletterListExport $export) => strtoupper($export->format)) ->orderColumn('created_at', 'created_at $1') ->orderColumn('row_count', 'row_count $1') ->orderColumn('format', 'format $1') ->rawColumns(['list']) ->make(true); } private function renderIndex(NewsletterListFilter $filter, ?NewsletterList $list): View { return view('admin.newsletter.index', [ 'filter' => $filter->toArray(), 'list' => $list, 'countries' => Country::query()->orderBy(app()->getLocale() === 'en' ? 'en' : 'de')->get(), 'members' => User::query() ->with('account') ->whereNotNull('account_id') ->where('admin', '<', 5) ->get() ->sortBy(fn (User $user) => mb_strtolower($user->getFullName(false))) ->values(), 'levels' => UserLevel::query()->orderBy('pos')->get(), 'languages' => ['de', 'en', 'es', 'fr'], 'sources' => NewsletterListFilter::sources(), 'optin_options' => NewsletterListFilter::optinOptions(), 'purchase_options' => NewsletterListFilter::purchaseOptions(), 'company_options' => NewsletterListFilter::companyOptions(), 'consultant_status_options' => NewsletterListFilter::consultantStatusOptions(), ]); } /** * Baut den Download, protokolliert den Export und aktualisiert die gespeicherte Liste. */ private function download(User $user, NewsletterListFilter $filter, string $format, ?NewsletterList $list): BinaryFileResponse|StreamedResponse { $result = $this->service->build($filter); $headers = $this->service->exportHeaders($filter->templateOnly); $rows = $this->service->exportRows($result->rows, $filter->templateOnly); $filename = 'newsletter_'.$filter->source.'_'.($filter->language ?? 'alle').'_'.now()->format('Y-m-d').'.'.$format; $this->logExport($user, $filter, $format, $filename, $result, $list); if ($format === 'csv') { return $this->streamCsv($filename, $headers, $rows); } return Excel::download(new xExport($rows, $headers), $filename); } private function logExport(User $user, NewsletterListFilter $filter, string $format, string $filename, NewsletterListResult $result, ?NewsletterList $list): void { NewsletterListExport::create([ 'newsletter_list_id' => $list?->id, 'user_id' => $user->id, 'format' => $format, 'row_count' => $result->stats['total'], 'filename' => $filename, 'filters' => $filter->toArray(), 'stats' => $result->stats, ]); if ($list !== null) { $list->forceFill([ 'last_exported_at' => now(), 'last_row_count' => $result->stats['total'], ])->save(); } } /** * @param string[] $headers * @param array> $rows */ private function streamCsv(string $filename, array $headers, array $rows): StreamedResponse { return response()->streamDownload(function () use ($headers, $rows) { $out = fopen('php://output', 'w'); fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $headers, ';', '"', '\\'); foreach ($rows as $row) { fputcsv($out, self::csvSafeRow($row), ';', '"', '\\'); } fclose($out); }, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']); } /** * Schützt vor CSV-Injection: Werte, die mit Formelzeichen beginnen, werden entschärft. * * @param array $row * @return array */ private static function csvSafeRow(array $row): array { return array_map(static function (string $value) { return $value !== '' && str_contains("=+-@\t\r", $value[0]) ? "'".$value : $value; }, $row); } private function savedActions(NewsletterList $list): string { return ' ' .' ' .' ' .'
' .'' .'
'; } /** * @param array $row */ private function optinBadge(array $row): string { $class = match ($row['optin']) { NewsletterListService::OPTIN_YES => 'badge-success', NewsletterListService::OPTIN_CONTRACT => 'badge-info', default => 'badge-danger', }; return ''.e($row['optin_label']).''; } /** * @param array $row */ private function statusBadge(array $row): string { $class = $row['status'] === NewsletterListService::STATUS_ACTIVE ? 'badge-success' : 'badge-secondary'; return ''.e($row['status_label']).''; } }