feat: add Weeztix OAuth, coupon codes, and Mailwizz mapping
Implement Weeztix integration per documentation: database config and subscriber coupon_code, OAuth redirect/callback, admin setup UI with company/coupon selection via AJAX, synchronous coupon creation on public subscribe with duplicate and rate-limit handling, Mailwizz field mapping for coupon codes, subscriber table and CSV export, and connection hint on the pages list. Made-with: Cursor
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
@php
|
||||
$config = $page->mailwizzConfig;
|
||||
$page->loadMissing('weeztixConfig');
|
||||
$hasWeeztixForCouponMap = $page->weeztixConfig !== null && $page->weeztixConfig->is_connected;
|
||||
$existing = $config !== null
|
||||
? [
|
||||
'list_uid' => $config->list_uid,
|
||||
@@ -8,6 +10,7 @@
|
||||
'field_first_name' => $config->field_first_name,
|
||||
'field_last_name' => $config->field_last_name,
|
||||
'field_phone' => $config->field_phone,
|
||||
'field_coupon_code' => $config->field_coupon_code,
|
||||
'tag_field' => $config->tag_field,
|
||||
'tag_value' => $config->tag_value,
|
||||
]
|
||||
@@ -27,6 +30,7 @@
|
||||
'csrf' => csrf_token(),
|
||||
'phoneEnabled' => $page->isPhoneFieldEnabledForSubscribers(),
|
||||
'hasExistingConfig' => $config !== null,
|
||||
'hasWeeztixIntegration' => $hasWeeztixForCouponMap,
|
||||
'existing' => $existing,
|
||||
'strings' => [
|
||||
'apiKeyRequired' => __('Enter your Mailwizz API key to continue.'),
|
||||
@@ -190,6 +194,16 @@
|
||||
</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
|
||||
@@ -244,6 +258,7 @@
|
||||
<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">
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
{{ __('Public URL') }}: <a href="{{ route('public.page', ['publicPage' => $page]) }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ url('/r/'.$page->slug) }}</a>
|
||||
</p>
|
||||
@can('update', $page)
|
||||
<p class="mt-3">
|
||||
<p class="mt-3 flex flex-wrap gap-x-4 gap-y-1">
|
||||
<a href="{{ route('admin.pages.mailwizz.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">{{ __('Mailwizz integration') }} →</a>
|
||||
<a href="{{ route('admin.pages.weeztix.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">{{ __('Weeztix integration') }} →</a>
|
||||
</p>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,12 @@
|
||||
@can('update', $page)
|
||||
<a href="{{ route('admin.pages.mailwizz.edit', $page) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Mailwizz') }}</a>
|
||||
@endcan
|
||||
@can('update', $page)
|
||||
<a href="{{ route('admin.pages.weeztix.edit', $page) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Weeztix') }}</a>
|
||||
@if ($page->weeztixConfig?->is_connected)
|
||||
<span class="text-xs font-medium text-emerald-600" title="{{ __('Weeztix verbonden') }}">●</span>
|
||||
@endif
|
||||
@endcan
|
||||
<button
|
||||
type="button"
|
||||
x-data="{ copied: false }"
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
@if ($page->isPhoneFieldEnabledForSubscribers())
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Phone') }}</th>
|
||||
@endif
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Kortingscode') }}</th>
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Registered at') }}</th>
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Mailwizz') }}</th>
|
||||
<th class="w-px whitespace-nowrap px-4 py-3 font-semibold text-slate-700">{{ __('Actions') }}</th>
|
||||
@@ -66,6 +67,7 @@
|
||||
@if ($page->isPhoneFieldEnabledForSubscribers())
|
||||
<td class="px-4 py-3 text-slate-600">{{ $subscriber->phoneDisplay() ?? '—' }}</td>
|
||||
@endif
|
||||
<td class="px-4 py-3 font-mono text-xs text-slate-700">{{ $subscriber->coupon_code ?? '—' }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $subscriber->created_at->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
||||
<td class="px-4 py-3">
|
||||
@if ($subscriber->synced_to_mailwizz)
|
||||
@@ -100,7 +102,7 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 7 : 6 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 8 : 7 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
{{ __('No subscribers match your criteria.') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
226
resources/views/admin/weeztix/edit.blade.php
Normal file
226
resources/views/admin/weeztix/edit.blade.php
Normal file
@@ -0,0 +1,226 @@
|
||||
@php
|
||||
use Illuminate\Support\Carbon;
|
||||
$wz = $page->weeztixConfig;
|
||||
$existing = $wz !== null
|
||||
? [
|
||||
'company_guid' => $wz->company_guid,
|
||||
'company_name' => $wz->company_name,
|
||||
'coupon_guid' => $wz->coupon_guid,
|
||||
'coupon_name' => $wz->coupon_name,
|
||||
'code_prefix' => $wz->code_prefix,
|
||||
'usage_count' => $wz->usage_count,
|
||||
]
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', __('Weeztix') . ' — ' . $page->title)
|
||||
|
||||
@section('mobile_title', __('Weeztix'))
|
||||
|
||||
@section('content')
|
||||
<div
|
||||
class="mx-auto max-w-3xl"
|
||||
x-data="weeztixSetup(@js([
|
||||
'pageId' => $page->id,
|
||||
'companiesUrl' => route('admin.weeztix.companies'),
|
||||
'couponsUrl' => route('admin.weeztix.coupons'),
|
||||
'csrf' => csrf_token(),
|
||||
'isConnected' => $wz?->is_connected ?? false,
|
||||
'tokenExpiresAt' => $wz?->token_expires_at instanceof Carbon ? $wz->token_expires_at->timezone(config('app.timezone'))->format('Y-m-d H:i') : null,
|
||||
'callbackUrl' => route('admin.weeztix.callback', absolute: true),
|
||||
'existing' => $existing,
|
||||
'strings' => [
|
||||
'genericError' => __('Er ging iets mis. Probeer het opnieuw.'),
|
||||
'selectCompany' => __('Selecteer een bedrijf.'),
|
||||
'loadCouponsError' => __('Kon kortingsbonnen niet laden.'),
|
||||
],
|
||||
]))"
|
||||
>
|
||||
<div class="mb-8">
|
||||
<a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Terug naar pagina') }}</a>
|
||||
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Weeztix') }}</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">{{ __('Pagina:') }} <span class="font-medium text-slate-800">{{ $page->title }}</span></p>
|
||||
</div>
|
||||
|
||||
@include('admin.pages._save_flash')
|
||||
|
||||
@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">
|
||||
<p class="font-medium">{{ __('Controleer het volgende:') }}</p>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||
@foreach ($errors->all() as $message)
|
||||
<li>{{ $message }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($wz !== null && $wz->is_connected)
|
||||
<div class="mb-8 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
<p class="font-medium">{{ __('Verbonden met Weeztix') }}</p>
|
||||
@if ($wz->token_expires_at)
|
||||
<p class="mt-1 text-emerald-800">
|
||||
{{ __('Toegangstoken verloopt om:') }}
|
||||
<span class="font-mono text-xs">{{ $wz->token_expires_at->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</span>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($wz !== null && ! $wz->is_connected)
|
||||
<div class="mb-8 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
||||
<p class="font-medium">{{ __('Niet verbonden') }}</p>
|
||||
<p class="mt-1 text-amber-900">{{ __('Je moet opnieuw verbinden om kortingscodes aan te maken. Gebruik de knop “Verbind met Weeztix”.') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($wz !== null)
|
||||
<form action="{{ route('admin.pages.weeztix.destroy', $page) }}" method="post" class="mb-8"
|
||||
onsubmit="return confirm(@js(__('Weeztix-integratie voor deze pagina verwijderen? Opgeslagen tokens worden gewist.')));">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-sm font-semibold text-red-700 underline hover:text-red-800">
|
||||
{{ __('Weeztix loskoppelen') }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
||||
<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>
|
||||
|
||||
<section class="space-y-4 border-b border-slate-100 pb-8">
|
||||
<h2 class="text-lg font-semibold text-slate-900">{{ __('Stap 1: OAuth-gegevens') }}</h2>
|
||||
<p class="text-sm leading-relaxed text-slate-600">
|
||||
{{ __('Maak eerst een OAuth-client in het Weeztix-dashboard en stel de redirect-URI exact in op:') }}
|
||||
</p>
|
||||
<p class="rounded-lg bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800 break-all" x-text="callbackUrl"></p>
|
||||
<p class="text-sm text-slate-600">
|
||||
{{ __('Maak daarna een korting (coupon) in Weeztix; die kies je hierna in stap 2.') }}
|
||||
</p>
|
||||
|
||||
<form method="post" action="{{ route('admin.pages.weeztix.update', $page) }}" class="space-y-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div>
|
||||
<label for="weeztix_client_id" class="block text-sm font-medium text-slate-700">{{ __('Client ID') }}</label>
|
||||
<input
|
||||
id="weeztix_client_id"
|
||||
name="client_id"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="{{ old('client_id') }}"
|
||||
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="{{ $wz !== null ? __('Laat leeg om ongewijzigd te laten') : '' }}"
|
||||
@if ($wz === null) required @endif
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="weeztix_client_secret" class="block text-sm font-medium text-slate-700">{{ __('Client secret') }}</label>
|
||||
<input
|
||||
id="weeztix_client_secret"
|
||||
name="client_secret"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
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="{{ $wz !== null ? __('Laat leeg om ongewijzigd te laten') : '' }}"
|
||||
@if ($wz === null) required @endif
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="rounded-lg bg-slate-800 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-700">
|
||||
{{ __('Gegevens opslaan') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if ($wz !== null)
|
||||
<div class="pt-2">
|
||||
<a
|
||||
href="{{ route('admin.pages.weeztix.oauth.redirect', $page) }}"
|
||||
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"
|
||||
>
|
||||
{{ __('Verbind met Weeztix') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
@if ($wz !== null)
|
||||
<section class="space-y-4 pt-8">
|
||||
<h2 class="text-lg font-semibold text-slate-900">{{ __('Stap 2: Bedrijf en kortingsbon') }}</h2>
|
||||
<p class="text-sm text-slate-600">{{ __('Na een geslaagde verbinding kun je een bedrijf en bestaande coupon uit Weeztix kiezen.') }}</p>
|
||||
|
||||
<form method="post" action="{{ route('admin.pages.weeztix.update', $page) }}" class="space-y-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div>
|
||||
<label for="weeztix_company" class="block text-sm font-medium text-slate-700">{{ __('Bedrijf') }}</label>
|
||||
<select
|
||||
id="weeztix_company"
|
||||
name="company_guid"
|
||||
x-model="companyGuid"
|
||||
@change="onCompanyChange()"
|
||||
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="">{{ __('Selecteer een bedrijf…') }}</option>
|
||||
<template x-for="c in companies" :key="c.guid">
|
||||
<option :value="c.guid" x-text="(c.name || c.guid)"></option>
|
||||
</template>
|
||||
</select>
|
||||
<input type="hidden" name="company_name" :value="companyName">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="weeztix_coupon" class="block text-sm font-medium text-slate-700">{{ __('Coupon (kortingssjabloon)') }}</label>
|
||||
<select
|
||||
id="weeztix_coupon"
|
||||
name="coupon_guid"
|
||||
x-model="couponGuid"
|
||||
@change="syncCouponName()"
|
||||
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="">{{ __('Selecteer een coupon…') }}</option>
|
||||
<template x-for="c in coupons" :key="c.guid">
|
||||
<option :value="c.guid" x-text="c.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<input type="hidden" name="coupon_name" :value="couponName">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="weeztix_code_prefix" class="block text-sm font-medium text-slate-700">{{ __('Codevoorvoegsel') }}</label>
|
||||
<input
|
||||
id="weeztix_code_prefix"
|
||||
name="code_prefix"
|
||||
type="text"
|
||||
maxlength="32"
|
||||
x-model="codePrefix"
|
||||
value="{{ old('code_prefix', $wz->code_prefix ?? 'PREREG') }}"
|
||||
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"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="weeztix_usage_count" class="block text-sm font-medium text-slate-700">{{ __('Gebruik per code') }}</label>
|
||||
<input
|
||||
id="weeztix_usage_count"
|
||||
name="usage_count"
|
||||
type="number"
|
||||
min="1"
|
||||
max="99999"
|
||||
x-model.number="usageCount"
|
||||
value="{{ old('usage_count', $wz->usage_count ?? 1) }}"
|
||||
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"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
|
||||
{{ __('Configuratie opslaan') }}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -156,6 +156,30 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="whitespace-pre-line text-center text-base leading-relaxed text-white/95 sm:text-lg" x-text="thankYouMessage"></p>
|
||||
<template x-if="couponCode">
|
||||
<div class="mt-6 rounded-xl border border-white/20 bg-white/10 p-6 backdrop-blur">
|
||||
<p class="text-center text-sm text-white/70">{{ __('Jouw kortingscode') }}</p>
|
||||
<div class="mt-3 flex flex-wrap items-center justify-center gap-3">
|
||||
<span
|
||||
class="font-mono text-2xl font-bold tracking-wider text-festival"
|
||||
x-text="couponCode"
|
||||
></span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-white/25 p-2 text-white/70 transition hover:border-white/40 hover:text-white"
|
||||
@click="copyCouponCode()"
|
||||
aria-label="{{ __('Kortingscode kopiëren') }}"
|
||||
>
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-3 text-center text-xs text-white/50">
|
||||
{{ __('Gebruik deze code bij het afrekenen in de ticketshop.') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<p
|
||||
x-show="redirectSecondsLeft !== null && redirectSecondsLeft > 0"
|
||||
x-cloak
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
'redirectUrl' => filled($redirectAfterSubmit) ? $redirectAfterSubmit : null,
|
||||
'strings' => [
|
||||
'linkCopied' => __('Link gekopieerd!'),
|
||||
'couponCopied' => __('Kortingscode gekopieerd!'),
|
||||
'redirectCountdown' => __('You will be redirected in :seconds s…'),
|
||||
],
|
||||
]))"
|
||||
|
||||
Reference in New Issue
Block a user