mivita/resources/views/admin/newsletter/index.blade.php
Kevin c858bd6ed0 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>
2026-09-23 15:44:09 +00:00

304 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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 +
' &middot; ' + statsLabels.customers + ': ' + s.customers +
' &middot; ' + statsLabels.consultants + ': ' + s.consultants +
' &middot; ' + statsLabels.duplicates + ': ' + s.duplicates +
' &middot; ' + 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