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 c568248046
commit c858bd6ed0
26 changed files with 3664 additions and 0 deletions

View file

@ -94,4 +94,8 @@ return [
'team_customers' => 'Team Kunden',
'payment_monitor' => 'Payment Monitor',
'payment_monitor_management' => 'Payment Monitor GF',
'newsletter_lists' => 'Newsletter-Listen',
'newsletter_create' => 'Liste erstellen',
'newsletter_saved' => 'Gespeicherte Listen',
'newsletter_exports' => 'Export-Protokoll',
];

View file

@ -0,0 +1,158 @@
<?php
return [
'title' => 'Newsletter-Listen',
'intro' => 'Empfängerliste aus Kunden und Beratern zusammenstellen, prüfen und für rapidmail exportieren. Gelöschte, anonymisierte, Fake- und Test-Datensätze werden immer ausgeschlossen, doppelte E-Mail-Adressen automatisch entfernt.',
'filter' => [
'source' => 'Quelle',
'language' => 'Sprache',
'country' => 'Land',
'optin' => 'Newsletter-Opt-in',
'purchase' => 'Kaufstatus',
'member' => 'Zugeordneter Berater / Sponsor',
'created_from' => 'Angelegt von',
'created_to' => 'Angelegt bis',
'company' => 'Firma',
'consultant_status' => 'Berater-Status',
'level' => 'Karriere-Level',
'include_admins' => 'Admin-Accounts einbeziehen',
'all' => 'alle',
'none_member' => 'ohne Zuordnung',
'consultant_section' => 'Berater-Filter',
'refresh' => 'Vorschau aktualisieren',
'reset' => 'Filter zurücksetzen',
],
'source_option' => [
'customers' => 'Kunden',
'consultants' => 'Berater',
'both' => 'Kunden und Berater',
],
'source' => [
'customer' => 'Kunde',
'consultant' => 'Berater',
],
'optin_option' => [
'all' => 'alle (Opt-in wird als Spalte ausgegeben)',
'subscribed' => 'nur mit Opt-in',
'unsubscribed' => 'nur ohne Opt-in',
],
'optin_value' => [
'yes' => 'ja',
'no' => 'nein',
'contract' => 'Vertrag',
],
'purchase_option' => [
'all' => 'alle',
'has_buyed' => 'hat gekauft',
'none' => 'noch nie gekauft',
'months_3' => 'gekauft in den letzten 3 Monaten',
'months_6' => 'gekauft in den letzten 6 Monaten',
'months_12' => 'gekauft in den letzten 12 Monaten',
'active_abo' => 'hat aktives Abo',
],
'company_option' => [
'all' => 'alle',
'with' => 'nur mit Firma',
'without' => 'nur ohne Firma',
],
'consultant_status_option' => [
'active' => 'nur aktive',
'inactive' => 'nur inaktive',
'all' => 'alle',
],
'status_value' => [
'active' => 'aktiv',
'inactive' => 'inaktiv',
],
'stats' => [
'total' => 'eindeutige Adressen',
'customers' => 'Kunden',
'consultants' => 'Berater',
'duplicates' => 'Duplikate entfernt',
'invalid' => 'ungültige Adressen',
'loading' => 'Liste wird berechnet …',
],
'hint' => [
'optin_all' => 'Die Liste enthält auch Kunden ohne Newsletter-Opt-in. Die Spalte „Newsletter" kennzeichnet den Status je Adresse.',
'consultants_contract' => 'Berater haben kein eigenes Opt-in; Grundlage ist der Beratervertrag (Spalte „Newsletter" = Vertrag).',
],
'export' => [
'email' => 'Emailadresse',
'firstname' => 'Vorname',
'lastname' => 'Nachname',
'salutation_label' => 'Anrede',
'street' => 'Straße',
'zipcode' => 'PLZ',
'city' => 'Wohnort',
'company' => 'Firma',
'consultant' => 'Berater',
'country' => 'Land',
'language' => 'Sprache',
'source_label' => 'Quelle',
'number' => 'Nummer',
'consultant_number' => 'Berater-Nr.',
'optin_label' => 'Newsletter',
'status_label' => 'Status',
'created_at' => 'Angelegt',
'button_xlsx' => 'Export XLSX',
'button_csv' => 'Export CSV',
'template_only' => 'Nur die 9 Spalten der Vorlage exportieren',
],
'table' => [
'email' => 'E-Mail',
'name' => 'Name',
'salutation' => 'Anrede',
'zipcode' => 'PLZ',
'city' => 'Ort',
'country' => 'Land',
'language' => 'Sprache',
'consultant' => 'Berater / Sponsor',
'source' => 'Quelle',
'optin' => 'Newsletter',
'status' => 'Status',
'created' => 'Angelegt',
],
'saved' => [
'title' => 'Gespeicherte Listen',
'current' => 'Geladene Liste',
'name' => 'Name',
'name_placeholder' => 'z. B. Newsletter DE Kunden',
'save_new' => 'Als neue Liste speichern',
'save_changes' => 'Änderungen in dieser Liste speichern',
'summary' => 'Filter',
'source' => 'Quelle',
'last_export' => 'Letzter Export',
'last_count' => 'Adressen',
'created_by' => 'Angelegt von',
'created_at' => 'Angelegt',
'actions' => 'Aktionen',
'load' => 'Filter laden',
'delete' => 'Löschen',
'confirm_delete' => 'Liste wirklich löschen? Das Export-Protokoll bleibt erhalten.',
'stored' => 'Liste gespeichert.',
'updated' => 'Liste aktualisiert.',
'deleted' => 'Liste gelöscht.',
'intro' => 'Gespeicherte Filterkombinationen. Über „Filter laden" wird die Liste in die Vorschau übernommen, der Export nutzt die hinterlegten Filter.',
],
'exports' => [
'title' => 'Export-Protokoll',
'intro' => 'Jeder Download wird mit Benutzer, Zeitpunkt, Filter und Anzahl der Adressen festgehalten.',
'date' => 'Datum',
'user' => 'Benutzer',
'list' => 'Liste',
'adhoc' => 'ohne gespeicherte Liste',
'summary' => 'Filter',
'format' => 'Format',
'rows' => 'Adressen',
'filename' => 'Datei',
],
'validation' => [
'source' => 'Ungültige Quelle.',
'option' => 'Ungültiger Filterwert.',
'member' => 'Ungültige Berater-Auswahl.',
'date' => 'Ungültiges Datum.',
'name' => 'Bitte einen Namen angeben (max. 120 Zeichen).',
'list' => 'Liste nicht gefunden.',
'date_range' => 'Das Enddatum darf nicht vor dem Startdatum liegen.',
],
];

View file

@ -94,4 +94,8 @@ return [
'team_customers' => 'Team Customers',
'payment_monitor' => 'Payment Monitor',
'payment_monitor_management' => 'Payment Monitor (Mgmt)',
'newsletter_lists' => 'Newsletter lists',
'newsletter_create' => 'Create list',
'newsletter_saved' => 'Saved lists',
'newsletter_exports' => 'Export log',
];

View file

@ -0,0 +1,158 @@
<?php
return [
'title' => 'Newsletter lists',
'intro' => 'Build a recipient list from customers and consultants, review it and export it for rapidmail. Deleted, anonymised, fake and test records are always excluded; duplicate e-mail addresses are removed automatically.',
'filter' => [
'source' => 'Source',
'language' => 'Language',
'country' => 'Country',
'optin' => 'Newsletter opt-in',
'purchase' => 'Purchase status',
'member' => 'Assigned consultant / sponsor',
'created_from' => 'Created from',
'created_to' => 'Created until',
'company' => 'Company',
'consultant_status' => 'Consultant status',
'level' => 'Career level',
'include_admins' => 'Include admin accounts',
'all' => 'all',
'none_member' => 'without assignment',
'consultant_section' => 'Consultant filters',
'refresh' => 'Refresh preview',
'reset' => 'Reset filters',
],
'source_option' => [
'customers' => 'Customers',
'consultants' => 'Consultants',
'both' => 'Customers and consultants',
],
'source' => [
'customer' => 'Customer',
'consultant' => 'Consultant',
],
'optin_option' => [
'all' => 'all (opt-in is exported as a column)',
'subscribed' => 'only with opt-in',
'unsubscribed' => 'only without opt-in',
],
'optin_value' => [
'yes' => 'yes',
'no' => 'no',
'contract' => 'contract',
],
'purchase_option' => [
'all' => 'all',
'has_buyed' => 'has purchased',
'none' => 'never purchased',
'months_3' => 'purchased in the last 3 months',
'months_6' => 'purchased in the last 6 months',
'months_12' => 'purchased in the last 12 months',
'active_abo' => 'has an active subscription',
],
'company_option' => [
'all' => 'all',
'with' => 'only with company',
'without' => 'only without company',
],
'consultant_status_option' => [
'active' => 'active only',
'inactive' => 'inactive only',
'all' => 'all',
],
'status_value' => [
'active' => 'active',
'inactive' => 'inactive',
],
'stats' => [
'total' => 'unique addresses',
'customers' => 'customers',
'consultants' => 'consultants',
'duplicates' => 'duplicates removed',
'invalid' => 'invalid addresses',
'loading' => 'Calculating list …',
],
'hint' => [
'optin_all' => 'The list also contains customers without newsletter opt-in. The "Newsletter" column marks the status per address.',
'consultants_contract' => 'Consultants have no separate opt-in; the legal basis is the consultant contract ("Newsletter" column = contract).',
],
'export' => [
'email' => 'Email address',
'firstname' => 'First name',
'lastname' => 'Last name',
'salutation_label' => 'Salutation',
'street' => 'Street',
'zipcode' => 'Postcode',
'city' => 'City',
'company' => 'Company',
'consultant' => 'Consultant',
'country' => 'Country',
'language' => 'Language',
'source_label' => 'Source',
'number' => 'Number',
'consultant_number' => 'Consultant no.',
'optin_label' => 'Newsletter',
'status_label' => 'Status',
'created_at' => 'Created',
'button_xlsx' => 'Export XLSX',
'button_csv' => 'Export CSV',
'template_only' => 'Export only the 9 template columns',
],
'table' => [
'email' => 'E-mail',
'name' => 'Name',
'salutation' => 'Salutation',
'zipcode' => 'Postcode',
'city' => 'City',
'country' => 'Country',
'language' => 'Language',
'consultant' => 'Consultant / sponsor',
'source' => 'Source',
'optin' => 'Newsletter',
'status' => 'Status',
'created' => 'Created',
],
'saved' => [
'title' => 'Saved lists',
'current' => 'Loaded list',
'name' => 'Name',
'name_placeholder' => 'e.g. Newsletter DE customers',
'save_new' => 'Save as new list',
'save_changes' => 'Save changes to this list',
'summary' => 'Filters',
'source' => 'Source',
'last_export' => 'Last export',
'last_count' => 'Addresses',
'created_by' => 'Created by',
'created_at' => 'Created',
'actions' => 'Actions',
'load' => 'Load filters',
'delete' => 'Delete',
'confirm_delete' => 'Really delete this list? The export log is kept.',
'stored' => 'List saved.',
'updated' => 'List updated.',
'deleted' => 'List deleted.',
'intro' => 'Saved filter combinations. "Load filters" puts the list into the preview; the export uses the stored filters.',
],
'exports' => [
'title' => 'Export log',
'intro' => 'Every download is recorded with user, time, filters and number of addresses.',
'date' => 'Date',
'user' => 'User',
'list' => 'List',
'adhoc' => 'without saved list',
'summary' => 'Filters',
'format' => 'Format',
'rows' => 'Addresses',
'filename' => 'File',
],
'validation' => [
'source' => 'Invalid source.',
'option' => 'Invalid filter value.',
'member' => 'Invalid consultant selection.',
'date' => 'Invalid date.',
'name' => 'Please enter a name (max. 120 characters).',
'list' => 'List not found.',
'date_range' => 'The end date must not be before the start date.',
],
];

View file

@ -95,4 +95,8 @@ return [
'my_statistics' => 'Mi estadística',
'team_statistics' => 'Estadística del equipo',
'withdrawals' => 'Desistimientos',
'newsletter_lists' => 'Listas de newsletter',
'newsletter_create' => 'Crear lista',
'newsletter_saved' => 'Listas guardadas',
'newsletter_exports' => 'Registro de exportaciones',
];

View file

@ -0,0 +1,158 @@
<?php
return [
'title' => 'Listas de newsletter',
'intro' => 'Crea una lista de destinatarios a partir de clientes y tutores, revísala y expórtala para rapidmail. Los registros eliminados, anonimizados, falsos y de prueba se excluyen siempre; las direcciones duplicadas se eliminan automáticamente.',
'filter' => [
'source' => 'Fuente',
'language' => 'Idioma',
'country' => 'País',
'optin' => 'Opt-in newsletter',
'purchase' => 'Estado de compra',
'member' => 'Tutor / patrocinador asignado',
'created_from' => 'Creado desde',
'created_to' => 'Creado hasta',
'company' => 'Empresa',
'consultant_status' => 'Estado del tutor',
'level' => 'Nivel de carrera',
'include_admins' => 'Incluir cuentas de administrador',
'all' => 'todos',
'none_member' => 'sin asignación',
'consultant_section' => 'Filtros de tutores',
'refresh' => 'Actualizar vista previa',
'reset' => 'Restablecer filtros',
],
'source_option' => [
'customers' => 'Clientes',
'consultants' => 'Tutores',
'both' => 'Clientes y tutores',
],
'source' => [
'customer' => 'Cliente',
'consultant' => 'Tutor',
],
'optin_option' => [
'all' => 'todos (el opt-in se exporta como columna)',
'subscribed' => 'solo con opt-in',
'unsubscribed' => 'solo sin opt-in',
],
'optin_value' => [
'yes' => 'sí',
'no' => 'no',
'contract' => 'contrato',
],
'purchase_option' => [
'all' => 'todos',
'has_buyed' => 'ha comprado',
'none' => 'nunca ha comprado',
'months_3' => 'compró en los últimos 3 meses',
'months_6' => 'compró en los últimos 6 meses',
'months_12' => 'compró en los últimos 12 meses',
'active_abo' => 'tiene una suscripción activa',
],
'company_option' => [
'all' => 'todos',
'with' => 'solo con empresa',
'without' => 'solo sin empresa',
],
'consultant_status_option' => [
'active' => 'solo activos',
'inactive' => 'solo inactivos',
'all' => 'todos',
],
'status_value' => [
'active' => 'activo',
'inactive' => 'inactivo',
],
'stats' => [
'total' => 'direcciones únicas',
'customers' => 'clientes',
'consultants' => 'tutores',
'duplicates' => 'duplicados eliminados',
'invalid' => 'direcciones no válidas',
'loading' => 'Calculando la lista …',
],
'hint' => [
'optin_all' => 'La lista también contiene clientes sin opt-in de newsletter. La columna «Newsletter» indica el estado de cada dirección.',
'consultants_contract' => 'Los tutores no tienen opt-in propio; la base es el contrato de tutor (columna «Newsletter» = contrato).',
],
'export' => [
'email' => 'Correo electrónico',
'firstname' => 'Nombre',
'lastname' => 'Apellidos',
'salutation_label' => 'Tratamiento',
'street' => 'Calle',
'zipcode' => 'Código postal',
'city' => 'Ciudad',
'company' => 'Empresa',
'consultant' => 'Tutor',
'country' => 'País',
'language' => 'Idioma',
'source_label' => 'Fuente',
'number' => 'Número',
'consultant_number' => 'N.º de tutor',
'optin_label' => 'Newsletter',
'status_label' => 'Estado',
'created_at' => 'Creado',
'button_xlsx' => 'Exportar XLSX',
'button_csv' => 'Exportar CSV',
'template_only' => 'Exportar solo las 9 columnas de la plantilla',
],
'table' => [
'email' => 'Correo',
'name' => 'Nombre',
'salutation' => 'Tratamiento',
'zipcode' => 'CP',
'city' => 'Ciudad',
'country' => 'País',
'language' => 'Idioma',
'consultant' => 'Tutor / patrocinador',
'source' => 'Fuente',
'optin' => 'Newsletter',
'status' => 'Estado',
'created' => 'Creado',
],
'saved' => [
'title' => 'Listas guardadas',
'current' => 'Lista cargada',
'name' => 'Nombre',
'name_placeholder' => 'p. ej. Newsletter DE clientes',
'save_new' => 'Guardar como lista nueva',
'save_changes' => 'Guardar cambios en esta lista',
'summary' => 'Filtros',
'source' => 'Fuente',
'last_export' => 'Última exportación',
'last_count' => 'Direcciones',
'created_by' => 'Creado por',
'created_at' => 'Creado',
'actions' => 'Acciones',
'load' => 'Cargar filtros',
'delete' => 'Eliminar',
'confirm_delete' => '¿Eliminar esta lista? El registro de exportaciones se conserva.',
'stored' => 'Lista guardada.',
'updated' => 'Lista actualizada.',
'deleted' => 'Lista eliminada.',
'intro' => 'Combinaciones de filtros guardadas. «Cargar filtros» lleva la lista a la vista previa; la exportación usa los filtros guardados.',
],
'exports' => [
'title' => 'Registro de exportaciones',
'intro' => 'Cada descarga se registra con usuario, fecha, filtros y número de direcciones.',
'date' => 'Fecha',
'user' => 'Usuario',
'list' => 'Lista',
'adhoc' => 'sin lista guardada',
'summary' => 'Filtros',
'format' => 'Formato',
'rows' => 'Direcciones',
'filename' => 'Archivo',
],
'validation' => [
'source' => 'Fuente no válida.',
'option' => 'Valor de filtro no válido.',
'member' => 'Selección de tutor no válida.',
'date' => 'Fecha no válida.',
'name' => 'Indique un nombre (máx. 120 caracteres).',
'list' => 'Lista no encontrada.',
'date_range' => 'La fecha final no puede ser anterior a la inicial.',
],
];

View file

@ -95,4 +95,8 @@ return [
'my_statistics' => 'Ma statistique',
'team_statistics' => 'Statistiques de l\'équipe',
'withdrawals' => 'Rétractations',
'newsletter_lists' => 'Listes newsletter',
'newsletter_create' => 'Créer une liste',
'newsletter_saved' => 'Listes enregistrées',
'newsletter_exports' => 'Journal des exports',
];

View file

@ -0,0 +1,158 @@
<?php
return [
'title' => 'Listes newsletter',
'intro' => 'Composez une liste de destinataires à partir des clients et des conseillers, vérifiez-la et exportez-la pour rapidmail. Les enregistrements supprimés, anonymisés, fictifs et de test sont toujours exclus ; les adresses en double sont retirées automatiquement.',
'filter' => [
'source' => 'Source',
'language' => 'Langue',
'country' => 'Pays',
'optin' => 'Opt-in newsletter',
'purchase' => 'Statut d’achat',
'member' => 'Conseiller / parrain attribué',
'created_from' => 'Créé à partir du',
'created_to' => 'Créé jusqu’au',
'company' => 'Société',
'consultant_status' => 'Statut du conseiller',
'level' => 'Niveau de carrière',
'include_admins' => 'Inclure les comptes administrateurs',
'all' => 'tous',
'none_member' => 'sans attribution',
'consultant_section' => 'Filtres conseillers',
'refresh' => 'Actualiser l’aperçu',
'reset' => 'Réinitialiser les filtres',
],
'source_option' => [
'customers' => 'Clients',
'consultants' => 'Conseillers',
'both' => 'Clients et conseillers',
],
'source' => [
'customer' => 'Client',
'consultant' => 'Conseiller',
],
'optin_option' => [
'all' => 'tous (l’opt-in est exporté comme colonne)',
'subscribed' => 'uniquement avec opt-in',
'unsubscribed' => 'uniquement sans opt-in',
],
'optin_value' => [
'yes' => 'oui',
'no' => 'non',
'contract' => 'contrat',
],
'purchase_option' => [
'all' => 'tous',
'has_buyed' => 'a acheté',
'none' => 'n’a jamais acheté',
'months_3' => 'a acheté au cours des 3 derniers mois',
'months_6' => 'a acheté au cours des 6 derniers mois',
'months_12' => 'a acheté au cours des 12 derniers mois',
'active_abo' => 'a un abonnement actif',
],
'company_option' => [
'all' => 'tous',
'with' => 'uniquement avec société',
'without' => 'uniquement sans société',
],
'consultant_status_option' => [
'active' => 'actifs uniquement',
'inactive' => 'inactifs uniquement',
'all' => 'tous',
],
'status_value' => [
'active' => 'actif',
'inactive' => 'inactif',
],
'stats' => [
'total' => 'adresses uniques',
'customers' => 'clients',
'consultants' => 'conseillers',
'duplicates' => 'doublons retirés',
'invalid' => 'adresses invalides',
'loading' => 'Calcul de la liste …',
],
'hint' => [
'optin_all' => 'La liste contient aussi des clients sans opt-in newsletter. La colonne « Newsletter » indique le statut de chaque adresse.',
'consultants_contract' => 'Les conseillers n’ont pas d’opt-in propre ; la base est le contrat de conseiller (colonne « Newsletter » = contrat).',
],
'export' => [
'email' => 'Adresse e-mail',
'firstname' => 'Prénom',
'lastname' => 'Nom',
'salutation_label' => 'Civilité',
'street' => 'Rue',
'zipcode' => 'Code postal',
'city' => 'Ville',
'company' => 'Société',
'consultant' => 'Conseiller',
'country' => 'Pays',
'language' => 'Langue',
'source_label' => 'Source',
'number' => 'Numéro',
'consultant_number' => 'N° conseiller',
'optin_label' => 'Newsletter',
'status_label' => 'Statut',
'created_at' => 'Créé le',
'button_xlsx' => 'Exporter XLSX',
'button_csv' => 'Exporter CSV',
'template_only' => 'Exporter uniquement les 9 colonnes du modèle',
],
'table' => [
'email' => 'E-mail',
'name' => 'Nom',
'salutation' => 'Civilité',
'zipcode' => 'CP',
'city' => 'Ville',
'country' => 'Pays',
'language' => 'Langue',
'consultant' => 'Conseiller / parrain',
'source' => 'Source',
'optin' => 'Newsletter',
'status' => 'Statut',
'created' => 'Créé le',
],
'saved' => [
'title' => 'Listes enregistrées',
'current' => 'Liste chargée',
'name' => 'Nom',
'name_placeholder' => 'p. ex. Newsletter DE clients',
'save_new' => 'Enregistrer comme nouvelle liste',
'save_changes' => 'Enregistrer les modifications dans cette liste',
'summary' => 'Filtres',
'source' => 'Source',
'last_export' => 'Dernier export',
'last_count' => 'Adresses',
'created_by' => 'Créé par',
'created_at' => 'Créé le',
'actions' => 'Actions',
'load' => 'Charger les filtres',
'delete' => 'Supprimer',
'confirm_delete' => 'Supprimer vraiment cette liste ? Le journal des exports est conservé.',
'stored' => 'Liste enregistrée.',
'updated' => 'Liste mise à jour.',
'deleted' => 'Liste supprimée.',
'intro' => 'Combinaisons de filtres enregistrées. « Charger les filtres » reprend la liste dans l’aperçu ; l’export utilise les filtres enregistrés.',
],
'exports' => [
'title' => 'Journal des exports',
'intro' => 'Chaque téléchargement est consigné avec l’utilisateur, l’heure, les filtres et le nombre d’adresses.',
'date' => 'Date',
'user' => 'Utilisateur',
'list' => 'Liste',
'adhoc' => 'sans liste enregistrée',
'summary' => 'Filtres',
'format' => 'Format',
'rows' => 'Adresses',
'filename' => 'Fichier',
],
'validation' => [
'source' => 'Source invalide.',
'option' => 'Valeur de filtre invalide.',
'member' => 'Sélection de conseiller invalide.',
'date' => 'Date invalide.',
'name' => 'Veuillez saisir un nom (120 caractères max.).',
'list' => 'Liste introuvable.',
'date_range' => 'La date de fin ne peut pas précéder la date de début.',
],
];

View file

@ -0,0 +1,51 @@
@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">
{{ __('newsletter.exports.title') }}
</h4>
<p class="text-muted">{{ __('newsletter.exports.intro') }}</p>
<div class="card">
<div class="card-datatable table-responsive">
<table class="table table-striped table-bordered" id="datatables-newsletter-exports">
<thead>
<tr>
<th>{{ __('newsletter.exports.date') }}</th>
<th>{{ __('newsletter.exports.user') }}</th>
<th>{{ __('newsletter.exports.list') }}</th>
<th>{{ __('newsletter.exports.summary') }}</th>
<th>{{ __('newsletter.exports.format') }}</th>
<th>{{ __('newsletter.exports.rows') }}</th>
<th>{{ __('newsletter.exports.filename') }}</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script>
$(document).ready(function () {
$('#datatables-newsletter-exports').DataTable({
processing: true,
serverSide: true,
ajax: { url: '{!! route('admin_newsletter_lists_exports_datatable') !!}' },
order: [[0, 'desc']],
columns: [
{ data: 'created_at', name: 'created_at', searchable: false },
{ data: 'user', name: 'user', orderable: false, searchable: false },
{ data: 'list', name: 'list', orderable: false, searchable: false },
{ data: 'summary', name: 'summary', orderable: false, searchable: false },
{ data: 'format', name: 'format' },
{ data: 'row_count', name: 'row_count', searchable: false },
{ data: 'filename', name: 'filename' }
],
bLengthChange: false,
iDisplayLength: 50,
language: { url: '/js/datatables-{{ \App::getLocale() }}.json' }
});
});
</script>
@endsection

View file

@ -0,0 +1,304 @@
@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

View file

@ -0,0 +1,56 @@
@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2">
{{ __('newsletter.saved.title') }}
</h4>
<p class="text-muted">{{ __('newsletter.saved.intro') }}</p>
<div class="card">
<div class="card-header">
<a href="{{ route('admin_newsletter_lists') }}" class="btn btn-sm btn-primary"><span class="fa fa-plus"></span> {{ __('navigation.newsletter_create') }}</a>
</div>
<div class="card-datatable table-responsive">
<table class="table table-striped table-bordered" id="datatables-newsletter-saved">
<thead>
<tr>
<th>{{ __('newsletter.saved.name') }}</th>
<th>{{ __('newsletter.saved.source') }}</th>
<th>{{ __('newsletter.saved.summary') }}</th>
<th>{{ __('newsletter.saved.last_export') }}</th>
<th>{{ __('newsletter.saved.last_count') }}</th>
<th>{{ __('newsletter.saved.created_by') }}</th>
<th>{{ __('newsletter.saved.created_at') }}</th>
<th>{{ __('newsletter.saved.actions') }}</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script>
$(document).ready(function () {
$('#datatables-newsletter-saved').DataTable({
processing: true,
serverSide: true,
ajax: { url: '{!! route('admin_newsletter_lists_saved_datatable') !!}' },
order: [[0, 'asc']],
columns: [
{ data: 'name', name: 'name' },
{ data: 'source', name: 'source' },
{ data: 'summary', name: 'summary', orderable: false, searchable: false },
{ data: 'last_exported_at', name: 'last_exported_at', searchable: false },
{ data: 'last_row_count', name: 'last_row_count', searchable: false },
{ data: 'creator', name: 'creator', orderable: false, searchable: false },
{ data: 'created_at', name: 'created_at', searchable: false },
{ data: 'actions', name: 'actions', orderable: false, searchable: false }
],
bLengthChange: false,
iDisplayLength: 50,
language: { url: '/js/datatables-{{ \App::getLocale() }}.json' }
});
});
</script>
@endsection

View file

@ -326,6 +326,32 @@
<div>{{ __('navigation.clients') }}</div>
</a>
</li>
<li class="sidenav-item @if (Request::is('admin/newsletter-lists*')) open @endif">
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
<i class="sidenav-icon ion ion-md-mail"></i>
<div>{{ __('navigation.newsletter_lists') }}</div>
</a>
<ul class="sidenav-menu">
<li class="sidenav-item{{ Request::is('admin/newsletter-lists', 'admin/newsletter-lists/load/*') ? ' active' : '' }}">
<a href="{{ route('admin_newsletter_lists') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-funnel"></i>
<div>{{ __('navigation.newsletter_create') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/newsletter-lists/saved*') ? ' active' : '' }}">
<a href="{{ route('admin_newsletter_lists_saved') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-bookmarks"></i>
<div>{{ __('navigation.newsletter_saved') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('admin/newsletter-lists/exports*') ? ' active' : '' }}">
<a href="{{ route('admin_newsletter_lists_exports') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-list-box"></i>
<div>{{ __('navigation.newsletter_exports') }}</div>
</a>
</li>
</ul>
</li>
<li class="sidenav-item @if (Request::is('admin/sales/*', 'admin/abos', 'admin/abos/*', 'admin/withdrawals', 'admin/withdrawals/*')) open @endif">
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
<i class="sidenav-icon ion ion-ios-cart"></i>