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:
2026-04-04 14:52:41 +02:00
parent 17e784fee7
commit d3abdb7ed9
30 changed files with 2272 additions and 5 deletions

View File

@@ -420,6 +420,7 @@ document.addEventListener('alpine:init', () => {
redirectSecondsLeft: null,
redirectTimer: null,
strings: config.strings || {},
couponCode: '',
copyPageLink() {
const url = this.pageShareUrl;
@@ -434,6 +435,19 @@ document.addEventListener('alpine:init', () => {
});
},
copyCouponCode() {
const code = this.couponCode;
if (!code) {
return;
}
navigator.clipboard.writeText(code).then(() => {
this.copyFeedback = this.strings?.couponCopied || '';
setTimeout(() => {
this.copyFeedback = '';
}, 2500);
});
},
init() {
if (this.phase === 'before') {
this.tickCountdown();
@@ -552,6 +566,8 @@ document.addEventListener('alpine:init', () => {
if (res.ok && data.success) {
this.phase = 'thanks';
this.thankYouMessage = data.message ?? '';
this.couponCode =
typeof data.coupon_code === 'string' && data.coupon_code !== '' ? data.coupon_code : '';
this.startRedirectCountdownIfNeeded();
return;
}
@@ -574,6 +590,7 @@ document.addEventListener('alpine:init', () => {
fieldsUrl: cfg.fieldsUrl,
phoneEnabled: cfg.phoneEnabled,
hasExistingConfig: cfg.hasExistingConfig,
hasWeeztixIntegration: cfg.hasWeeztixIntegration === true,
existing: cfg.existing,
csrf: cfg.csrf,
step: 1,
@@ -586,6 +603,7 @@ document.addEventListener('alpine:init', () => {
fieldFirstName: '',
fieldLastName: '',
fieldPhone: '',
fieldCouponCode: '',
tagField: '',
tagValue: '',
loading: false,
@@ -597,6 +615,7 @@ document.addEventListener('alpine:init', () => {
this.fieldFirstName = this.existing.field_first_name ?? '';
this.fieldLastName = this.existing.field_last_name ?? '';
this.fieldPhone = this.existing.field_phone ?? '';
this.fieldCouponCode = this.existing.field_coupon_code ?? '';
this.tagField = this.existing.tag_field ?? '';
this.tagValue = this.existing.tag_value ?? '';
this.selectedListUid = this.existing.list_uid ?? '';
@@ -706,6 +725,7 @@ document.addEventListener('alpine:init', () => {
this.fieldFirstName = this.existing.field_first_name || this.fieldFirstName;
this.fieldLastName = this.existing.field_last_name || this.fieldLastName;
this.fieldPhone = this.existing.field_phone || this.fieldPhone;
this.fieldCouponCode = this.existing.field_coupon_code || this.fieldCouponCode;
this.tagField = this.existing.tag_field || this.tagField;
this.tagValue = this.existing.tag_value || this.tagValue;
}
@@ -746,6 +766,104 @@ document.addEventListener('alpine:init', () => {
this.$refs.saveForm.requestSubmit();
},
}));
Alpine.data('weeztixSetup', (cfg) => ({
pageId: cfg.pageId,
companiesUrl: cfg.companiesUrl,
couponsUrl: cfg.couponsUrl,
csrf: cfg.csrf,
isConnected: cfg.isConnected === true,
callbackUrl: cfg.callbackUrl,
errorMessage: '',
companies: [],
coupons: [],
companyGuid: '',
companyName: '',
couponGuid: '',
couponName: '',
codePrefix: 'PREREG',
usageCount: 1,
strings: cfg.strings || {},
async init() {
if (cfg.existing) {
this.codePrefix = cfg.existing.code_prefix || 'PREREG';
this.usageCount =
typeof cfg.existing.usage_count === 'number' ? cfg.existing.usage_count : 1;
this.companyGuid = cfg.existing.company_guid || '';
this.companyName = cfg.existing.company_name || '';
this.couponGuid = cfg.existing.coupon_guid || '';
this.couponName = cfg.existing.coupon_name || '';
}
if (this.isConnected) {
await this.loadCompanies();
if (this.companyGuid) {
await this.loadCouponsForGuid(this.companyGuid);
}
}
},
async postJson(url, body) {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': this.csrf,
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
return { res, data };
},
syncCompanyNameFromSelection() {
const c = this.companies.find((x) => x.guid === this.companyGuid);
this.companyName = c && c.name ? c.name : '';
},
syncCouponName() {
const c = this.coupons.find((x) => x.guid === this.couponGuid);
this.couponName = c ? c.name : '';
},
async loadCompanies() {
this.errorMessage = '';
const { res, data } = await this.postJson(this.companiesUrl, { page_id: this.pageId });
if (!res.ok) {
this.errorMessage = data.message || this.strings.genericError;
return;
}
this.companies = Array.isArray(data.companies) ? data.companies : [];
this.syncCompanyNameFromSelection();
},
async onCompanyChange() {
this.syncCompanyNameFromSelection();
this.couponGuid = '';
this.couponName = '';
this.coupons = [];
if (!this.companyGuid) {
return;
}
await this.loadCouponsForGuid(this.companyGuid);
},
async loadCouponsForGuid(guid) {
this.errorMessage = '';
const { res, data } = await this.postJson(this.couponsUrl, {
page_id: this.pageId,
company_guid: guid,
});
if (!res.ok) {
this.errorMessage = data.message || this.strings.loadCouponsError;
return;
}
this.coupons = Array.isArray(data.coupons) ? data.coupons : [];
this.syncCouponName();
},
}));
});
window.Alpine = Alpine;