feat: Mailwizz overview vs wizard flow and wizard step guard

Load Weeztix config for coupon mapping context, redirect incomplete
configs to step one, and expand admin Mailwizz UI and tests.

Made-with: Cursor
This commit is contained in:
2026-04-05 13:34:00 +02:00
parent 1e7ee14540
commit 91caa16e70
4 changed files with 385 additions and 214 deletions

View File

@@ -8,18 +8,29 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UpdateMailwizzConfigRequest; use App\Http\Requests\Admin\UpdateMailwizzConfigRequest;
use App\Models\PreregistrationPage; use App\Models\PreregistrationPage;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\View\View; use Illuminate\View\View;
class MailwizzController extends Controller class MailwizzController extends Controller
{ {
public function edit(PreregistrationPage $page): View public function edit(Request $request, PreregistrationPage $page): View|RedirectResponse
{ {
$this->authorize('update', $page); $this->authorize('update', $page);
$page->load('mailwizzConfig'); $page->load(['mailwizzConfig', 'weeztixConfig']);
return view('admin.mailwizz.edit', compact('page')); $config = $page->mailwizzConfig;
$showWizard = $config === null || $request->boolean('wizard');
if ($showWizard && $config === null) {
$requestedStep = min(4, max(1, (int) $request->query('step', 1)));
if ($requestedStep !== 1) {
return redirect()
->route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]);
}
}
return view('admin.mailwizz.edit', compact('page', 'showWizard'));
} }
public function update(UpdateMailwizzConfigRequest $request, PreregistrationPage $page): RedirectResponse public function update(UpdateMailwizzConfigRequest $request, PreregistrationPage $page): RedirectResponse

View File

@@ -14,6 +14,7 @@
"Sending…": "Bezig met verzenden…", "Sending…": "Bezig met verzenden…",
"Something went wrong. Please try again.": "Er ging iets mis. Probeer het opnieuw.", "Something went wrong. Please try again.": "Er ging iets mis. Probeer het opnieuw.",
"This pre-registration period has ended.": "Deze preregistratieperiode is afgelopen.", "This pre-registration period has ended.": "Deze preregistratieperiode is afgelopen.",
"You will be redirected in :seconds s…": "Je wordt over :seconds seconden doorgestuurd…",
"Visit ticket shop": "Ga naar de ticketshop", "Visit ticket shop": "Ga naar de ticketshop",
"Thank you for registering!": "Bedankt voor je registratie!", "Thank you for registering!": "Bedankt voor je registratie!",
"You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.", "You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.",

View File

@@ -2,6 +2,7 @@
$config = $page->mailwizzConfig; $config = $page->mailwizzConfig;
$page->loadMissing('weeztixConfig'); $page->loadMissing('weeztixConfig');
$hasWeeztixForCouponMap = $page->weeztixConfig !== null && $page->weeztixConfig->is_connected; $hasWeeztixForCouponMap = $page->weeztixConfig !== null && $page->weeztixConfig->is_connected;
$mailwizzStatus = $page->mailwizzIntegrationStatus();
$existing = $config !== null $existing = $config !== null
? [ ? [
'list_uid' => $config->list_uid, 'list_uid' => $config->list_uid,
@@ -24,30 +25,15 @@
@section('mobile_title', __('Mailwizz')) @section('mobile_title', __('Mailwizz'))
@section('content') @section('content')
<div class="mx-auto max-w-3xl" x-data="mailwizzWizard(@js([ <div class="mx-auto max-w-3xl">
'listsUrl' => route('admin.mailwizz.lists'),
'fieldsUrl' => route('admin.mailwizz.fields'),
'csrf' => csrf_token(),
'phoneEnabled' => $page->isPhoneFieldEnabledForSubscribers(),
'hasExistingConfig' => $config !== null,
'hasWeeztixIntegration' => $hasWeeztixForCouponMap,
'existing' => $existing,
'strings' => [
'apiKeyRequired' => __('Enter your Mailwizz API key to continue.'),
'genericError' => __('Something went wrong. Please try again.'),
'noListsError' => __('No mailing lists were returned. Check your API key or create a list in Mailwizz.'),
'selectListError' => __('Select a mailing list.'),
'mapFieldsError' => __('Map email, first name, and last name to Mailwizz fields.'),
'tagFieldError' => __('Select a checkbox list field for source / tag tracking.'),
'tagValueError' => __('Select the tag option that identifies this pre-registration.'),
],
]))">
<div class="mb-8"> <div class="mb-8">
<a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500"> {{ __('Back to page') }}</a> <a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500"> {{ __('Back to page') }}</a>
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Mailwizz') }}</h1> <h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Mailwizz') }}</h1>
<p class="mt-2 text-sm text-slate-600">{{ __('Page:') }} <span class="font-medium text-slate-800">{{ $page->title }}</span></p> <p class="mt-2 text-sm text-slate-600">{{ __('Page:') }} <span class="font-medium text-slate-800">{{ $page->title }}</span></p>
</div> </div>
@include('admin.pages._save_flash')
@if ($errors->any()) @if ($errors->any())
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900" role="alert"> <div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900" role="alert">
<p class="font-medium">{{ __('Please fix the following:') }}</p> <p class="font-medium">{{ __('Please fix the following:') }}</p>
@@ -59,219 +45,347 @@
</div> </div>
@endif @endif
@if ($config !== null) @if (! $showWizard && $config !== null)
<div class="mb-8 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900"> @if ($mailwizzStatus !== 'ready')
<p class="font-medium">{{ __('Integration active') }}</p> <div class="mb-6 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
<p class="mt-1 text-emerald-800"> <p class="font-medium">{{ __('Setup incomplete') }}</p>
{{ __('List:') }} <p class="mt-1 text-amber-900">{{ __('Run the wizard again to finish Mailwizz (API key, list, and field mapping).') }}</p>
<span class="font-mono text-xs">{{ $config->list_name ?: $config->list_uid }}</span> </div>
</p> @endif
<form action="{{ route('admin.pages.mailwizz.destroy', $page) }}" method="post" class="mt-3"
<div class="mb-6 flex flex-wrap items-center gap-3">
<a
href="{{ route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]) }}"
class="inline-flex rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
>
{{ __('Change settings (wizard)') }}
</a>
<form action="{{ route('admin.pages.mailwizz.destroy', $page) }}" method="post" class="inline"
onsubmit="return confirm(@js(__('Remove Mailwizz integration for this page? Subscribers will stay in the database but will no longer sync.')));"> onsubmit="return confirm(@js(__('Remove Mailwizz integration for this page? Subscribers will stay in the database but will no longer sync.')));">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="text-sm font-semibold text-red-700 underline hover:text-red-800"> <button type="submit" class="rounded-lg border border-red-200 bg-white px-4 py-2.5 text-sm font-semibold text-red-700 shadow-sm hover:bg-red-50">
{{ __('Remove integration') }} {{ __('Disconnect Mailwizz') }}
</button> </button>
</form> </form>
</div> </div>
@endif
<div class="mb-6 flex flex-wrap gap-2 text-xs font-medium text-slate-500"> <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
<span :class="step >= 1 ? 'text-indigo-600' : ''">1. {{ __('API key') }}</span> <h2 class="text-lg font-semibold text-slate-900">{{ __('Current configuration') }}</h2>
<span aria-hidden="true"></span> <p class="mt-1 text-sm text-slate-600">{{ __('The API key is stored encrypted and is not shown here.') }}</p>
<span :class="step >= 2 ? 'text-indigo-600' : ''">2. {{ __('List') }}</span>
<span aria-hidden="true"></span>
<span :class="step >= 3 ? 'text-indigo-600' : ''">3. {{ __('Field mapping') }}</span>
<span aria-hidden="true"></span>
<span :class="step >= 4 ? 'text-indigo-600' : ''">4. {{ __('Tag / source') }}</span>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8"> <dl class="mt-6 space-y-4 border-t border-slate-100 pt-6 text-sm">
<div x-show="errorMessage !== ''" x-cloak class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" x-text="errorMessage"></div> <div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Connection') }}</dt>
<dd>
@if ($mailwizzStatus === 'ready')
<span class="inline-flex rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800">{{ __('Ready to sync') }}</span>
@else
<span class="inline-flex rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-900">{{ __('Incomplete') }}</span>
@endif
</dd>
</div>
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Mailing list') }}</dt>
<dd class="text-slate-800">{{ $config->list_name ?: '—' }}</dd>
</div>
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('List UID') }}</dt>
<dd class="break-all font-mono text-xs text-slate-600">{{ $config->list_uid ?: '—' }}</dd>
</div>
</dl>
{{-- Step 1 --}} <h3 class="mt-8 border-t border-slate-100 pt-6 text-sm font-semibold text-slate-900">{{ __('Field mapping') }}</h3>
<div x-show="step === 1" x-cloak class="space-y-4"> <p class="mt-1 text-xs text-slate-500">{{ __('Mailwizz custom fields are matched by tag.') }}</p>
<p class="text-sm leading-relaxed text-slate-600"> <dl class="mt-4 space-y-4 text-sm">
{{ __('First, create a mailing list in Mailwizz with the required custom fields. Add a custom field of type Checkbox List with an option value you will use to track this pre-registration source.') }} <div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
</p> <dt class="font-medium text-slate-700">{{ __('Email') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ $config->field_email ?: '—' }}</dd>
</div>
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('First name') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ $config->field_first_name ?: '—' }}</dd>
</div>
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Last name') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ $config->field_last_name ?: '—' }}</dd>
</div>
@if ($page->isPhoneFieldEnabledForSubscribers())
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Phone') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ filled($config->field_phone) ? $config->field_phone : '—' }}</dd>
</div>
@endif
@if ($hasWeeztixForCouponMap)
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Kortingscode (Weeztix)') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ filled($config->field_coupon_code) ? $config->field_coupon_code : '—' }}</dd>
</div>
@endif
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Tag / source (checkbox list)') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ filled($config->tag_field) ? $config->tag_field : '—' }}</dd>
</div>
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
<dt class="font-medium text-slate-700">{{ __('Source tag option') }}</dt>
<dd class="font-mono text-xs text-slate-800">{{ filled($config->tag_value) ? $config->tag_value : '—' }}</dd>
</div>
</dl>
</div>
@else
<div
x-data="mailwizzWizard(@js([
'listsUrl' => route('admin.mailwizz.lists'),
'fieldsUrl' => route('admin.mailwizz.fields'),
'csrf' => csrf_token(),
'phoneEnabled' => $page->isPhoneFieldEnabledForSubscribers(),
'hasExistingConfig' => $config !== null,
'hasWeeztixIntegration' => $hasWeeztixForCouponMap,
'existing' => $existing,
'strings' => [
'apiKeyRequired' => __('Enter your Mailwizz API key to continue.'),
'genericError' => __('Something went wrong. Please try again.'),
'noListsError' => __('No mailing lists were returned. Check your API key or create a list in Mailwizz.'),
'selectListError' => __('Select a mailing list.'),
'mapFieldsError' => __('Map email, first name, and last name to Mailwizz fields.'),
'tagFieldError' => __('Select a checkbox list field for source / tag tracking.'),
'tagValueError' => __('Select the tag option that identifies this pre-registration.'),
],
]))"
>
@if ($config !== null) @if ($config !== null)
<p class="text-sm text-amber-800"> <div class="mb-6 flex flex-wrap items-center gap-3">
{{ __('Enter your API key and connect to load Mailwizz data (the same key as before is fine). If you clear the key field before saving, the previously stored key is kept.') }} <a href="{{ route('admin.pages.mailwizz.edit', $page) }}" class="text-sm font-medium text-slate-600 hover:text-slate-900">
</p> {{ __('Cancel and return to overview') }}
</a>
</div>
@endif @endif
<div>
<label for="mailwizz_api_key" class="block text-sm font-medium text-slate-700">{{ __('Mailwizz API key') }}</label> <div class="mb-8 flex flex-wrap items-center gap-2" aria-label="{{ __('Wizard steps') }}">
<input <span
id="mailwizz_api_key" class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
type="password" :class="step === 1 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : (step > 1 ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : 'border-slate-200 bg-slate-50 text-slate-500')"
autocomplete="off"
x-model="apiKey"
class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
placeholder="{{ __('Paste API key') }}"
> >
</div> <span class="tabular-nums">1</span>
<button {{ __('API key') }}
type="button" </span>
class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50" <span class="text-slate-300" aria-hidden="true"></span>
:disabled="loading" <span
@click="connectLists()" class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
> :class="step === 2 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : (step > 2 ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : 'border-slate-200 bg-slate-50 text-slate-500')"
<span x-show="!loading">{{ __('Connect & load lists') }}</span>
<span x-show="loading" x-cloak>{{ __('Connecting…') }}</span>
</button>
</div>
{{-- Step 2 --}}
<div x-show="step === 2" x-cloak class="space-y-4">
<div>
<label for="mailwizz_list" class="block text-sm font-medium text-slate-700">{{ __('Mailing list') }}</label>
<select
id="mailwizz_list"
x-model="selectedListUid"
@change="syncListNameFromSelection()"
class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
> >
<option value="">{{ __('Select a list…') }}</option> <span class="tabular-nums">2</span>
<template x-for="list in lists" :key="list.list_uid"> {{ __('List') }}
<option :value="list.list_uid" x-text="list.name"></option> </span>
</template> <span class="text-slate-300" aria-hidden="true"></span>
</select> <span
</div> class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
<div class="flex flex-wrap gap-3"> :class="step === 3 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : (step > 3 ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : 'border-slate-200 bg-slate-50 text-slate-500')"
<button type="button" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50" @click="step = 1">
{{ __('Back') }}
</button>
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50"
:disabled="loading"
@click="loadFieldsAndGoStep3()"
> >
<span x-show="!loading">{{ __('Load fields') }}</span> <span class="tabular-nums">3</span>
<span x-show="loading" x-cloak>{{ __('Loading') }}</span> {{ __('Field mapping') }}
</button> </span>
</div> <span class="text-slate-300" aria-hidden="true"></span>
</div> <span
class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
{{-- Step 3 --}} :class="step === 4 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : 'border-slate-200 bg-slate-50 text-slate-500'"
<div x-show="step === 3" x-cloak class="space-y-5">
<p class="text-sm text-slate-600">{{ __('Map each local field to the matching Mailwizz custom field (by tag).') }}</p>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Email') }}</label>
<select x-model="fieldEmail" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in emailFieldChoices()" :key="f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('First name') }}</label>
<select x-model="fieldFirstName" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in textFields()" :key="'fn-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Last name') }}</label>
<select x-model="fieldLastName" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in textFields()" :key="'ln-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div x-show="phoneEnabled">
<label class="block text-sm font-medium text-slate-700">{{ __('Phone') }} <span class="font-normal text-slate-500">({{ __('optional') }})</span></label>
<select x-model="fieldPhone" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in phoneFields()" :key="'ph-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div x-show="hasWeeztixIntegration">
<label class="block text-sm font-medium text-slate-700">{{ __('Kortingscode (Weeztix)') }} <span class="font-normal text-slate-500">({{ __('optional') }})</span></label>
<p class="mt-1 text-xs text-slate-500">{{ __('Koppel aan een tekstveld in Mailwizz om de persoonlijke code in e-mails te tonen.') }}</p>
<select x-model="fieldCouponCode" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in textFields()" :key="'cp-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Tag / source (checkbox list)') }}</label>
<select
x-model="tagField"
@change="tagValue = ''"
class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
> >
<option value="">{{ __('Select checkbox list field…') }}</option> <span class="tabular-nums">4</span>
<template x-for="f in checkboxFields()" :key="'cb-' + f.tag"> {{ __('Tag / source') }}
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option> </span>
</template>
</select>
</div> </div>
<p x-show="checkboxFields().length === 0" class="text-sm text-amber-800">
{{ __('No checkbox list fields were returned for this list. Add one in Mailwizz, then run “Load fields” again from step 2.') }}
</p>
<div class="flex flex-wrap gap-3"> <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
<button type="button" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50" @click="step = 2"> <div x-show="errorMessage !== ''" x-cloak class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" x-text="errorMessage"></div>
{{ __('Back') }}
</button>
<button type="button" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500" @click="goStep4()">
{{ __('Continue') }}
</button>
</div>
</div>
{{-- Step 4 --}} {{-- Step 1 --}}
<div x-show="step === 4" x-cloak class="space-y-5"> <div x-show="step === 1" x-cloak class="space-y-4">
<p class="text-sm text-slate-600">{{ __('Choose the checkbox option that marks subscribers from this pre-registration page.') }}</p> <h2 class="text-lg font-semibold text-slate-900">{{ __('Step 1: API key') }}</h2>
<p class="text-sm leading-relaxed text-slate-600">
<fieldset class="space-y-2"> {{ __('First, create a mailing list in Mailwizz with the required custom fields. Add a custom field of type Checkbox List with an option value you will use to track this pre-registration source.') }}
<legend class="sr-only">{{ __('Tag value') }}</legend> </p>
<template x-for="opt in tagOptionsList()" :key="opt.key"> @if ($config !== null)
<label class="flex cursor-pointer items-start gap-3 rounded-lg border border-slate-200 p-3 hover:bg-slate-50"> <p class="text-sm text-amber-800">
<input type="radio" name="tag_value_choice" class="mt-1 text-indigo-600" :value="opt.key" x-model="tagValue"> {{ __('Enter your API key and connect to load Mailwizz data (the same key as before is fine). If you clear the key field before saving, the previously stored key is kept.') }}
<span class="text-sm text-slate-800" x-text="opt.label"></span> </p>
</label> @endif
</template> <div>
</fieldset> <label for="mailwizz_api_key" class="block text-sm font-medium text-slate-700">{{ __('Mailwizz API key') }}</label>
<p x-show="tagField && tagOptionsList().length === 0" class="text-sm text-amber-800"> <input
{{ __('This field has no options defined in Mailwizz. Add options to the checkbox list field, then reload fields.') }} id="mailwizz_api_key"
</p> type="password"
autocomplete="off"
<form x-ref="saveForm" method="post" action="{{ route('admin.pages.mailwizz.update', $page) }}" class="space-y-4"> x-model="apiKey"
@csrf class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
@method('PUT') placeholder="{{ __('Paste API key') }}"
<input type="hidden" name="api_key" x-bind:value="apiKey"> >
<input type="hidden" name="list_uid" x-bind:value="selectedListUid"> </div>
<input type="hidden" name="list_name" x-bind:value="selectedListName"> <button
<input type="hidden" name="field_email" x-bind:value="fieldEmail"> type="button"
<input type="hidden" name="field_first_name" x-bind:value="fieldFirstName"> class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50"
<input type="hidden" name="field_last_name" x-bind:value="fieldLastName"> :disabled="loading"
<input type="hidden" name="field_phone" x-bind:value="phoneEnabled ? fieldPhone : ''"> @click="connectLists()"
<input type="hidden" name="field_coupon_code" x-bind:value="hasWeeztixIntegration ? fieldCouponCode : ''"> >
<input type="hidden" name="tag_field" x-bind:value="tagField"> <span x-show="!loading">{{ __('Connect & load lists') }}</span>
<input type="hidden" name="tag_value" x-bind:value="tagValue"> <span x-show="loading" x-cloak>{{ __('Connecting…') }}</span>
<div class="flex flex-wrap gap-3">
<button type="button" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50" @click="step = 3">
{{ __('Back') }}
</button>
<button type="button" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500" @click="submitSave()">
{{ __('Save configuration') }}
</button> </button>
</div> </div>
</form>
{{-- Step 2 --}}
<div x-show="step === 2" x-cloak class="space-y-4">
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 2: Mailing list') }}</h2>
<div>
<label for="mailwizz_list" class="block text-sm font-medium text-slate-700">{{ __('Mailing list') }}</label>
<select
id="mailwizz_list"
x-model="selectedListUid"
@change="syncListNameFromSelection()"
class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<option value="">{{ __('Select a list…') }}</option>
<template x-for="list in lists" :key="list.list_uid">
<option :value="list.list_uid" x-text="list.name"></option>
</template>
</select>
</div>
<div class="flex flex-wrap gap-3">
<button type="button" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50" @click="step = 1">
{{ __('Back') }}
</button>
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50"
:disabled="loading"
@click="loadFieldsAndGoStep3()"
>
<span x-show="!loading">{{ __('Load fields') }}</span>
<span x-show="loading" x-cloak>{{ __('Loading…') }}</span>
</button>
</div>
</div>
{{-- Step 3 --}}
<div x-show="step === 3" x-cloak class="space-y-5">
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 3: Field mapping') }}</h2>
<p class="text-sm text-slate-600">{{ __('Map each local field to the matching Mailwizz custom field (by tag).') }}</p>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Email') }}</label>
<select x-model="fieldEmail" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in emailFieldChoices()" :key="f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('First name') }}</label>
<select x-model="fieldFirstName" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in textFields()" :key="'fn-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Last name') }}</label>
<select x-model="fieldLastName" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in textFields()" :key="'ln-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div x-show="phoneEnabled">
<label class="block text-sm font-medium text-slate-700">{{ __('Phone') }} <span class="font-normal text-slate-500">({{ __('optional') }})</span></label>
<select x-model="fieldPhone" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in phoneFields()" :key="'ph-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div x-show="hasWeeztixIntegration">
<label class="block text-sm font-medium text-slate-700">{{ __('Kortingscode (Weeztix)') }} <span class="font-normal text-slate-500">({{ __('optional') }})</span></label>
<p class="mt-1 text-xs text-slate-500">{{ __('Koppel aan een tekstveld in Mailwizz om de persoonlijke code in e-mails te tonen.') }}</p>
<select x-model="fieldCouponCode" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="">{{ __('Select field…') }}</option>
<template x-for="f in textFields()" :key="'cp-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Tag / source (checkbox list)') }}</label>
<select
x-model="tagField"
@change="tagValue = ''"
class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<option value="">{{ __('Select checkbox list field…') }}</option>
<template x-for="f in checkboxFields()" :key="'cb-' + f.tag">
<option :value="f.tag" x-text="f.label + ' (' + f.tag + ')'"></option>
</template>
</select>
</div>
<p x-show="checkboxFields().length === 0" class="text-sm text-amber-800">
{{ __('No checkbox list fields were returned for this list. Add one in Mailwizz, then run “Load fields” again from step 2.') }}
</p>
<div class="flex flex-wrap gap-3">
<button type="button" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50" @click="step = 2">
{{ __('Back') }}
</button>
<button type="button" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500" @click="goStep4()">
{{ __('Continue') }}
</button>
</div>
</div>
{{-- Step 4 --}}
<div x-show="step === 4" x-cloak class="space-y-5">
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 4: Tag / source') }}</h2>
<p class="text-sm text-slate-600">{{ __('Choose the checkbox option that marks subscribers from this pre-registration page.') }}</p>
<fieldset class="space-y-2">
<legend class="sr-only">{{ __('Tag value') }}</legend>
<template x-for="opt in tagOptionsList()" :key="opt.key">
<label class="flex cursor-pointer items-start gap-3 rounded-lg border border-slate-200 p-3 hover:bg-slate-50">
<input type="radio" name="tag_value_choice" class="mt-1 text-indigo-600" :value="opt.key" x-model="tagValue">
<span class="text-sm text-slate-800" x-text="opt.label"></span>
</label>
</template>
</fieldset>
<p x-show="tagField && tagOptionsList().length === 0" class="text-sm text-amber-800">
{{ __('This field has no options defined in Mailwizz. Add options to the checkbox list field, then reload fields.') }}
</p>
<form x-ref="saveForm" method="post" action="{{ route('admin.pages.mailwizz.update', $page) }}" class="space-y-4">
@csrf
@method('PUT')
<input type="hidden" name="api_key" x-bind:value="apiKey">
<input type="hidden" name="list_uid" x-bind:value="selectedListUid">
<input type="hidden" name="list_name" x-bind:value="selectedListName">
<input type="hidden" name="field_email" x-bind:value="fieldEmail">
<input type="hidden" name="field_first_name" x-bind:value="fieldFirstName">
<input type="hidden" name="field_last_name" x-bind:value="fieldLastName">
<input type="hidden" name="field_phone" x-bind:value="phoneEnabled ? fieldPhone : ''">
<input type="hidden" name="field_coupon_code" x-bind:value="hasWeeztixIntegration ? fieldCouponCode : ''">
<input type="hidden" name="tag_field" x-bind:value="tagField">
<input type="hidden" name="tag_value" x-bind:value="tagValue">
<div class="flex flex-wrap gap-3">
<button type="button" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50" @click="step = 3">
{{ __('Back') }}
</button>
<button type="button" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500" @click="submitSave()">
{{ __('Save configuration') }}
</button>
</div>
</form>
</div>
</div>
</div> </div>
</div> @endif
</div> </div>
@endsection @endsection

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\MailwizzConfig;
use App\Models\PreregistrationPage; use App\Models\PreregistrationPage;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -36,6 +37,50 @@ class MailwizzConfigUiTest extends TestCase
$response->assertForbidden(); $response->assertForbidden();
} }
public function test_connected_mailwizz_shows_overview_until_wizard_requested(): void
{
$user = User::factory()->create(['role' => 'user']);
$page = $this->makePageForUser($user);
MailwizzConfig::query()->create([
'preregistration_page_id' => $page->id,
'api_key' => 'test-key',
'list_uid' => 'list-uid-1',
'list_name' => 'Main list',
'field_email' => 'EMAIL',
'field_first_name' => 'FNAME',
'field_last_name' => 'LNAME',
'field_phone' => null,
'field_coupon_code' => null,
'tag_field' => 'TAGS',
'tag_value' => 'preregister-source',
]);
$overview = $this->actingAs($user)->get(route('admin.pages.mailwizz.edit', $page));
$overview->assertOk();
$overview->assertSee('Current configuration', escape: false);
$overview->assertSee('Change settings (wizard)', escape: false);
$overview->assertDontSee('Step 1: API key', escape: false);
$wizard = $this->actingAs($user)->get(route('admin.pages.mailwizz.edit', [
'page' => $page,
'wizard' => 1,
'step' => 1,
]));
$wizard->assertOk();
$wizard->assertSee('Step 1: API key', escape: false);
$wizard->assertSee('Cancel and return to overview', escape: false);
}
public function test_mailwizz_wizard_redirects_to_step_one_when_no_config_and_step_gt_one(): void
{
$user = User::factory()->create(['role' => 'user']);
$page = $this->makePageForUser($user);
$this->actingAs($user)
->get(route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 3]))
->assertRedirect(route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]));
}
private function makePageForUser(User $user): PreregistrationPage private function makePageForUser(User $user): PreregistrationPage
{ {
return PreregistrationPage::query()->create([ return PreregistrationPage::query()->create([