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>
304 lines
18 KiB
PHP
304 lines
18 KiB
PHP
@extends('layouts.layout-2')
|
||
|
||
@section('content')
|
||
|
||
<h4 class="font-weight-bold py-2 mb-2">
|
||
{{ __('newsletter.title') }}
|
||
</h4>
|
||
<p class="text-muted">{{ __('newsletter.intro') }}</p>
|
||
|
||
@if($list)
|
||
<div class="alert alert-secondary py-2 d-flex align-items-center justify-content-between flex-wrap">
|
||
<div>
|
||
<strong>{{ __('newsletter.saved.current') }}:</strong> {{ $list->name }}
|
||
<span class="text-muted small ml-2">
|
||
@if($list->last_exported_at)
|
||
{{ __('newsletter.saved.last_export') }}: {{ $list->last_exported_at->format('d.m.Y H:i') }} ({{ $list->last_row_count }} {{ __('newsletter.saved.last_count') }})
|
||
@endif
|
||
</span>
|
||
</div>
|
||
<a href="{{ route('admin_newsletter_lists_saved') }}" class="btn btn-sm btn-outline-secondary">{{ __('newsletter.saved.title') }}</a>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<form id="newsletter-filter" method="get" action="{{ route('admin_newsletter_lists') }}" onsubmit="return false;">
|
||
<input type="hidden" name="refresh" id="filter_refresh" value="0">
|
||
<input type="hidden" name="list_id" value="{{ $list?->id }}">
|
||
<div class="form-row">
|
||
<div class="col-md-12 mb-3">
|
||
<label class="form-label d-block">{{ __('newsletter.filter.source') }}</label>
|
||
@foreach($sources as $source)
|
||
<div class="custom-control custom-radio custom-control-inline">
|
||
<input type="radio" class="custom-control-input" id="source_{{ $source }}" name="source" value="{{ $source }}" @if($filter['source'] === $source) checked @endif>
|
||
<label class="custom-control-label" for="source_{{ $source }}">{{ __('newsletter.source_option.'.$source) }}</label>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="col-md-2 mb-3">
|
||
<label class="form-label" for="filter_language">{{ __('newsletter.filter.language') }}</label>
|
||
<select class="custom-select" name="language" id="filter_language">
|
||
<option value="" @if($filter['language'] === '') selected @endif>{{ __('newsletter.filter.all') }}</option>
|
||
@foreach($languages as $language)
|
||
<option value="{{ $language }}" @if($filter['language'] === $language) selected @endif>{{ strtoupper($language) }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<label class="form-label" for="filter_country_ids">{{ __('newsletter.filter.country') }}</label>
|
||
<select class="selectpicker form-control" data-style="btn-default" data-live-search="true" data-actions-box="true" multiple name="country_ids[]" id="filter_country_ids" title="{{ __('newsletter.filter.all') }}">
|
||
@foreach($countries as $country)
|
||
<option value="{{ $country->id }}" @if(in_array($country->id, $filter['country_ids'], true)) selected @endif>{{ $country->code }} – {{ $country->getLocated() }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<label class="form-label" for="filter_optin">{{ __('newsletter.filter.optin') }}</label>
|
||
<select class="custom-select" name="optin" id="filter_optin">
|
||
@foreach($optin_options as $option)
|
||
<option value="{{ $option }}" @if($filter['optin'] === $option) selected @endif>{{ __('newsletter.optin_option.'.$option) }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label" for="filter_purchase">{{ __('newsletter.filter.purchase') }}</label>
|
||
<select class="custom-select" name="purchase" id="filter_purchase">
|
||
@foreach($purchase_options as $option)
|
||
<option value="{{ $option }}" @if($filter['purchase'] === $option) selected @endif>{{ __('newsletter.purchase_option.'.$option) }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label" for="filter_member_id">{{ __('newsletter.filter.member') }}</label>
|
||
<select class="selectpicker form-control" data-style="btn-default" data-live-search="true" name="member_id" id="filter_member_id">
|
||
<option value="" @if($filter['member_id'] === '') selected @endif>{{ __('newsletter.filter.all') }}</option>
|
||
<option value="none" @if($filter['member_id'] === 'none') selected @endif>{{ __('newsletter.filter.none_member') }}</option>
|
||
@foreach($members as $member)
|
||
<option value="{{ $member->id }}" @if((string) $filter['member_id'] === (string) $member->id) selected @endif>{{ $member->getFullName() }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-2 mb-3">
|
||
<label class="form-label" for="filter_created_from">{{ __('newsletter.filter.created_from') }}</label>
|
||
<input type="date" class="form-control" name="created_from" id="filter_created_from" value="{{ $filter['created_from'] }}">
|
||
</div>
|
||
<div class="col-md-2 mb-3">
|
||
<label class="form-label" for="filter_created_to">{{ __('newsletter.filter.created_to') }}</label>
|
||
<input type="date" class="form-control" name="created_to" id="filter_created_to" value="{{ $filter['created_to'] }}">
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label" for="filter_company">{{ __('newsletter.filter.company') }}</label>
|
||
<select class="custom-select" name="company" id="filter_company">
|
||
@foreach($company_options as $option)
|
||
<option value="{{ $option }}" @if($filter['company'] === $option) selected @endif>{{ __('newsletter.company_option.'.$option) }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="consultant-filters" class="border-top pt-3 mt-1" @if($filter['source'] === 'customers') style="display:none" @endif>
|
||
<div class="small font-weight-semibold text-muted mb-2">{{ __('newsletter.filter.consultant_section') }}</div>
|
||
<div class="form-row">
|
||
<div class="col-md-3 mb-3">
|
||
<label class="form-label" for="filter_consultant_status">{{ __('newsletter.filter.consultant_status') }}</label>
|
||
<select class="custom-select" name="consultant_status" id="filter_consultant_status">
|
||
@foreach($consultant_status_options as $option)
|
||
<option value="{{ $option }}" @if($filter['consultant_status'] === $option) selected @endif>{{ __('newsletter.consultant_status_option.'.$option) }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label" for="filter_level_ids">{{ __('newsletter.filter.level') }}</label>
|
||
<select class="selectpicker form-control" data-style="btn-default" data-actions-box="true" multiple name="level_ids[]" id="filter_level_ids" title="{{ __('newsletter.filter.all') }}">
|
||
@foreach($levels as $level)
|
||
<option value="{{ $level->id }}" @if(in_array($level->id, $filter['level_ids'], true)) selected @endif>{{ $level->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3 mb-3 pt-4">
|
||
<div class="custom-control custom-checkbox">
|
||
<input type="checkbox" class="custom-control-input" name="include_admins" id="filter_include_admins" value="1" @if($filter['include_admins']) checked @endif>
|
||
<label class="custom-control-label" for="filter_include_admins">{{ __('newsletter.filter.include_admins') }}</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row align-items-center border-top pt-3">
|
||
<div class="col-md-8 mb-2">
|
||
<button type="button" class="btn btn-primary" id="filter_apply"><span class="fa fa-sync"></span> {{ __('newsletter.filter.refresh') }}</button>
|
||
<a href="{{ route('admin_newsletter_lists') }}?reset=filter" class="btn btn-outline-dark">{{ __('newsletter.filter.reset') }}</a>
|
||
</div>
|
||
<div class="col-md-4 mb-2 text-right">
|
||
<div class="custom-control custom-checkbox d-inline-block mr-3">
|
||
<input type="checkbox" class="custom-control-input" name="template_only" id="filter_template_only" value="1" @if($filter['template_only']) checked @endif>
|
||
<label class="custom-control-label" for="filter_template_only">{{ __('newsletter.export.template_only') }}</label>
|
||
</div>
|
||
<a href="#" class="btn btn-success export-link" data-format="xlsx"><span class="fa fa-file-excel"></span> {{ __('newsletter.export.button_xlsx') }}</a>
|
||
<a href="#" class="btn btn-outline-success export-link" data-format="csv"><span class="fa fa-file-csv"></span> {{ __('newsletter.export.button_csv') }}</a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<form id="newsletter-save-form" method="post" action="{{ route('admin_newsletter_lists_store') }}" class="form-row align-items-center border-top pt-3 mt-2">
|
||
@csrf
|
||
<div id="newsletter-save-fields"></div>
|
||
<div class="col-md-5 mb-2">
|
||
<input type="text" class="form-control" name="name" maxlength="120" required placeholder="{{ __('newsletter.saved.name_placeholder') }}" value="{{ old('name', $list?->name) }}">
|
||
@error('name')<div class="text-danger small">{{ $message }}</div>@enderror
|
||
</div>
|
||
<div class="col-md-7 mb-2">
|
||
@if($list)
|
||
<button type="submit" class="btn btn-outline-primary" data-action="{{ route('admin_newsletter_lists_update', [$list->id]) }}"><span class="fa fa-save"></span> {{ __('newsletter.saved.save_changes') }}</button>
|
||
@endif
|
||
<button type="submit" class="btn btn-outline-primary" data-action="{{ route('admin_newsletter_lists_store') }}"><span class="fa fa-plus"></span> {{ __('newsletter.saved.save_new') }}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card-body py-2">
|
||
<div class="alert alert-warning py-2 mb-2" id="hint_optin_all" @if($filter['optin'] !== 'all') style="display:none" @endif>{{ __('newsletter.hint.optin_all') }}</div>
|
||
<div class="alert alert-info py-2 mb-2" id="hint_consultants" @if($filter['source'] === 'customers') style="display:none" @endif>{{ __('newsletter.hint.consultants_contract') }}</div>
|
||
<div id="newsletter-stats" class="text-muted small">{{ __('newsletter.stats.loading') }}</div>
|
||
</div>
|
||
|
||
<div class="card-datatable table-responsive">
|
||
<table class="table table-striped table-bordered" id="datatables-newsletter">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ __('newsletter.table.email') }}</th>
|
||
<th>{{ __('newsletter.table.name') }}</th>
|
||
<th>{{ __('newsletter.table.salutation') }}</th>
|
||
<th>{{ __('newsletter.table.zipcode') }}</th>
|
||
<th>{{ __('newsletter.table.city') }}</th>
|
||
<th>{{ __('newsletter.table.country') }}</th>
|
||
<th>{{ __('newsletter.table.language') }}</th>
|
||
<th>{{ __('newsletter.table.consultant') }}</th>
|
||
<th>{{ __('newsletter.table.source') }}</th>
|
||
<th>{{ __('newsletter.table.optin') }}</th>
|
||
<th>{{ __('newsletter.table.status') }}</th>
|
||
<th>{{ __('newsletter.table.created') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
$(document).ready(function () {
|
||
var form = $('#newsletter-filter');
|
||
var statsLabels = {
|
||
total: @json(__('newsletter.stats.total')),
|
||
customers: @json(__('newsletter.stats.customers')),
|
||
consultants: @json(__('newsletter.stats.consultants')),
|
||
duplicates: @json(__('newsletter.stats.duplicates')),
|
||
invalid: @json(__('newsletter.stats.invalid'))
|
||
};
|
||
|
||
function filterParams(d) {
|
||
d = d || {};
|
||
form.serializeArray().forEach(function (field) {
|
||
if (field.name.slice(-2) === '[]') {
|
||
var key = field.name.slice(0, -2);
|
||
(d[key] = d[key] || []).push(field.value);
|
||
} else {
|
||
d[field.name] = field.value;
|
||
}
|
||
});
|
||
return d;
|
||
}
|
||
|
||
var oTable = $('#datatables-newsletter').DataTable({
|
||
processing: true,
|
||
serverSide: true,
|
||
ajax: {
|
||
url: '{!! route('admin_newsletter_lists_datatable') !!}',
|
||
data: function (d) {
|
||
filterParams(d);
|
||
}
|
||
},
|
||
order: [[0, 'asc']],
|
||
columns: [
|
||
{ data: 'email', name: 'email' },
|
||
{ data: 'name', name: 'name', orderable: false },
|
||
{ data: 'salutation_label', name: 'salutation_label' },
|
||
{ data: 'zipcode', name: 'zipcode' },
|
||
{ data: 'city', name: 'city' },
|
||
{ data: 'country', name: 'country' },
|
||
{ data: 'language', name: 'language' },
|
||
{ data: 'consultant', name: 'consultant' },
|
||
{ data: 'source_label', name: 'source_label' },
|
||
{ data: 'optin_label', name: 'optin_label' },
|
||
{ data: 'status_label', name: 'status_label' },
|
||
{ data: 'created_at', name: 'created_at' }
|
||
],
|
||
bLengthChange: false,
|
||
iDisplayLength: 50,
|
||
language: {
|
||
url: '/js/datatables-{{ \App::getLocale() }}.json'
|
||
}
|
||
});
|
||
|
||
$('#datatables-newsletter').on('xhr.dt', function (e, settings, json) {
|
||
$('#filter_refresh').val('0');
|
||
if (!json || !json.stats) {
|
||
return;
|
||
}
|
||
var s = json.stats;
|
||
$('#newsletter-stats').html(
|
||
'<strong>' + s.total + '</strong> ' + statsLabels.total +
|
||
' · ' + statsLabels.customers + ': ' + s.customers +
|
||
' · ' + statsLabels.consultants + ': ' + s.consultants +
|
||
' · ' + statsLabels.duplicates + ': ' + s.duplicates +
|
||
' · ' + statsLabels.invalid + ': ' + s.invalid
|
||
);
|
||
});
|
||
|
||
function syncHints() {
|
||
var source = form.find('input[name=source]:checked').val();
|
||
$('#consultant-filters').toggle(source !== 'customers');
|
||
$('#hint_consultants').toggle(source !== 'customers');
|
||
$('#hint_optin_all').toggle($('#filter_optin').val() === 'all' && source !== 'consultants');
|
||
}
|
||
|
||
$('#filter_apply').on('click', function () {
|
||
$('#filter_refresh').val('1');
|
||
syncHints();
|
||
oTable.draw();
|
||
});
|
||
|
||
form.find('select, input[type=date], input[type=checkbox], input[type=radio]').on('change', function () {
|
||
syncHints();
|
||
oTable.draw();
|
||
});
|
||
|
||
$('#newsletter-save-form button[type=submit]').on('click', function () {
|
||
$('#newsletter-save-form').attr('action', $(this).data('action'));
|
||
});
|
||
|
||
$('#newsletter-save-form').on('submit', function () {
|
||
var fields = $('#newsletter-save-fields').empty();
|
||
form.serializeArray().forEach(function (field) {
|
||
if (field.name === 'refresh' || field.name === 'list_id') {
|
||
return;
|
||
}
|
||
fields.append($('<input>', { type: 'hidden', name: field.name, value: field.value }));
|
||
});
|
||
});
|
||
|
||
$('.export-link').on('click', function (e) {
|
||
e.preventDefault();
|
||
var base = '{!! route('admin_newsletter_lists_export', ['format' => '__FORMAT__']) !!}'.replace('__FORMAT__', $(this).data('format'));
|
||
window.location.href = base + '?' + $.param(filterParams({}));
|
||
});
|
||
});
|
||
</script>
|
||
@endsection
|