Newsletter-Listen: Empfaengerlisten filtern, speichern und exportieren

Admin-Bereich "Newsletter-Listen" (Plan dev/_tickets/entwicklungsplan-
newsletter-listen.md): Berater und Kunden nach Filtern zusammenstellen,
als Liste speichern und wieder laden, Export als XLSX/CSV. Jeder Export
wird protokolliert (newsletter_list_exports).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-23 15:44:09 +00:00
parent ef7e333bd4
commit d21d94a060
26 changed files with 3664 additions and 0 deletions

View file

@ -0,0 +1,75 @@
<?php
namespace App\Http\Requests\Newsletter;
use App\Services\Newsletter\NewsletterListFilter;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class NewsletterListFilterRequest extends FormRequest
{
public function authorize(): bool
{
return (bool) $this->user()?->isAdmin();
}
/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'source' => ['nullable', Rule::in(NewsletterListFilter::sources())],
'language' => ['nullable', 'string', 'max:2'],
'country_ids' => ['nullable', 'array'],
'country_ids.*' => ['integer'],
'optin' => ['nullable', Rule::in(NewsletterListFilter::optinOptions())],
'purchase' => ['nullable', Rule::in(NewsletterListFilter::purchaseOptions())],
'member_id' => ['nullable', 'regex:/^(none|\d+)$/'],
'created_from' => ['nullable', 'date'],
'created_to' => ['nullable', 'date', 'after_or_equal:created_from'],
'company' => ['nullable', Rule::in(NewsletterListFilter::companyOptions())],
'consultant_status' => ['nullable', Rule::in(NewsletterListFilter::consultantStatusOptions())],
'level_ids' => ['nullable', 'array'],
'level_ids.*' => ['integer'],
'include_admins' => ['nullable', 'boolean'],
'template_only' => ['nullable', 'boolean'],
'refresh' => ['nullable', 'boolean'],
'list_id' => ['nullable', 'integer', 'exists:newsletter_lists,id'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'source.in' => __('newsletter.validation.source'),
'optin.in' => __('newsletter.validation.option'),
'purchase.in' => __('newsletter.validation.option'),
'company.in' => __('newsletter.validation.option'),
'consultant_status.in' => __('newsletter.validation.option'),
'member_id.regex' => __('newsletter.validation.member'),
'created_from.date' => __('newsletter.validation.date'),
'created_to.date' => __('newsletter.validation.date'),
'created_to.after_or_equal' => __('newsletter.validation.date_range'),
'country_ids.*.integer' => __('newsletter.validation.option'),
'level_ids.*.integer' => __('newsletter.validation.option'),
'list_id.exists' => __('newsletter.validation.list'),
'list_id.integer' => __('newsletter.validation.list'),
];
}
public function toFilter(): NewsletterListFilter
{
return NewsletterListFilter::fromArray($this->validated());
}
public function listId(): ?int
{
$id = $this->validated('list_id');
return $id === null || $id === '' ? null : (int) $id;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Newsletter;
/**
* Speichern einer Filterkombination unter einem Namen. Erbt die Filter-Regeln.
*/
class StoreNewsletterListRequest extends NewsletterListFilterRequest
{
/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return parent::rules() + [
'name' => ['required', 'string', 'max:120'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return parent::messages() + [
'name.required' => __('newsletter.validation.name'),
'name.max' => __('newsletter.validation.name'),
];
}
public function listName(): string
{
return trim((string) $this->validated('name'));
}
}