Check the setup chain at the top of the settings form

A "setup check" box lists whether the booking form has the expected
field names (essential ones flagged as errors), the booking page carries
the booking form module, confirmation and inquiry pages are wired, the
calendar's reader page shows the booking status module and at least one
notification is assigned. Green when complete, red with concrete hints
otherwise, so misconfigurations show up before the first booking.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jürgen MummertandClaude Fable 5.1 committed 2026-09-26 18:01:22 +02:00
1 parent fdaff1b1d1
commit 36a317d32e
5 files changed
+231 -1

No files matched your search

+9 -1
View File
@@ -40,7 +40,8 @@ $GLOBALS['TL_DCA']['tl_wk_config'] = [
],
],
'palettes' => [
'default' => '{title_legend},title,calendar;'
'default' => '{check_legend},setupCheck;'
.'{title_legend},title,calendar;'
.'{page_legend},bookingForm,bookingPage,confirmationPage,inquiryPage;'
.'{booking_legend},deadlineDays,deadlineDisplayDays,ownVenues;'
.'{import_legend},author,instructorGroup,speakerGroup;'
@@ -176,6 +177,13 @@ $GLOBALS['TL_DCA']['tl_wk_config'] = [
],
];
// Text und Box setzt der SetupCheckListener beim Bearbeiten
$GLOBALS['TL_DCA']['tl_wk_config']['fields']['setupCheck'] = [
'label' => ['', ''],
'inputType' => 'wk_explanation',
'eval' => ['tl_class' => 'long clr'],
];
$GLOBALS['TL_DCA']['tl_wk_config']['fields']['notificationOverview'] = [
'label' => ['', ''],
'inputType' => 'wk_explanation',
+16
View File
@@ -70,3 +70,19 @@ $lang['notification9'] = ['Bedingung 9: Evangelische Schulen, mehrtägig', 'Kate
$lang['notification10'] = ['Bedingung 10: Mentoring, eintägig', 'Kategorie-Art „Mentoring“, Beginn = Ende. Leer = Standard.'];
$lang['notification11'] = ['Bedingung 11: Mentoring, mehrtägig', 'Kategorie-Art „Mentoring“, Beginn ≠ Ende. Leer = Standard.'];
$lang['notification12'] = ['Bedingung 12: Ausbildung, mehrtägig', 'Kategorie-Art „Ausbildung“, Beginn ≠ Ende. Eintägige Ausbildungen erhalten die Standard-Benachrichtigung. Leer = Standard.'];
$lang['check_legend'] = 'Prüfung der Einrichtung';
$lang['check']['calendar_missing'] = 'Kein Kalender gewählt. Ohne Kalender greifen Import und Anmeldung nicht.';
$lang['check']['booking_form_missing'] = 'Kein Anmeldeformular gewählt. Das Bundle greift nur bei dem hier gewählten Formular ein (Felder je Veranstaltung, Plätze, Speichern, Benachrichtigung).';
$lang['check']['booking_form_essential'] = 'Im Anmeldeformular „%s“ fehlen Pflichtfelder: %s. Ohne sie lassen sich Anmeldungen nicht zuordnen oder bestätigen. Die Feldnamen müssen den Spalten der Anmeldung entsprechen.';
$lang['check']['booking_form_incomplete'] = 'Das Anmeldeformular „%s“ hat keine Felder für: %s. Diese Angaben werden dann nicht abgefragt und nicht gespeichert (kann gewollt sein).';
$lang['check']['booking_form_ok'] = 'Das Anmeldeformular „%s“ enthält alle Felder der Anmeldung.';
$lang['check']['booking_page_missing'] = 'Keine Seite mit dem Anmeldeformular gewählt. Der Link „Jetzt anmelden“ führt sonst ins Leere.';
$lang['check']['booking_page_ok'] = 'Die Seite „%s“ enthält das Modul „W&K-Anmeldeformular“.';
$lang['check']['booking_page_module_missing'] = 'Auf der Seite „%s“ liegt kein Modul „W&K-Anmeldeformular“ (weder in einem Artikel noch in einem Seitenlayout).';
$lang['check']['confirmation_page_missing'] = 'Keine Bestätigungsseite gewählt. Nach der Anmeldung wird dorthin weitergeleitet.';
$lang['check']['inquiry_page_ok'] = 'Die Seite „%s“ enthält ein Formular, das in die Tabelle tl_wk_inquiry speichert.';
$lang['check']['inquiry_page_form_missing'] = 'Auf der Seite „%s“ liegt kein Formular mit „Daten in Tabelle speichern“ → tl_wk_inquiry. Kontaktanfragen landen so nicht im Posteingang.';
$lang['check']['reader_status_missing'] = 'Auf der Weiterleitungsseite des Kalenders „%s“ liegt kein Modul „W&K-Anmeldestatus“ (freie Plätze, Anmeldeschluss, Link „Jetzt anmelden“).';
$lang['check']['notifications_missing'] = 'Keine Benachrichtigung zugeordnet. Anmeldungen werden gespeichert, aber es wird keine E-Mail verschickt.';
$lang['check']['all_ok'] = 'Alles eingerichtet.';
+16
View File
@@ -70,3 +70,19 @@ $lang['notification9'] = ['Condition 9: evangelical schools, multi-day', 'Kind "
$lang['notification10'] = ['Condition 10: mentoring, single day', 'Kind "mentoring", start = end. Empty = default.'];
$lang['notification11'] = ['Condition 11: mentoring, multi-day', 'Kind "mentoring", start ≠ end. Empty = default.'];
$lang['notification12'] = ['Condition 12: training, multi-day', 'Kind "training", start ≠ end. Single-day trainings get the default notification. Empty = default.'];
$lang['check_legend'] = 'Setup check';
$lang['check']['calendar_missing'] = 'No calendar selected. Import and bookings only work with a calendar.';
$lang['check']['booking_form_missing'] = 'No booking form selected. The bundle only hooks into the form selected here (fields per event, places, storing, notification).';
$lang['check']['booking_form_essential'] = 'The booking form "%s" lacks required fields: %s. Without them bookings cannot be assigned or confirmed. Field names must match the booking columns.';
$lang['check']['booking_form_incomplete'] = 'The booking form "%s" has no fields for: %s. These values are neither asked nor stored (may be intended).';
$lang['check']['booking_form_ok'] = 'The booking form "%s" contains all booking fields.';
$lang['check']['booking_page_missing'] = 'No page with the booking form selected, so the "book now" link leads nowhere.';
$lang['check']['booking_page_ok'] = 'Page "%s" contains the "W&K booking form" module.';
$lang['check']['booking_page_module_missing'] = 'Page "%s" contains no "W&K booking form" module (neither in an article nor in a page layout).';
$lang['check']['confirmation_page_missing'] = 'No confirmation page selected. Bookings redirect there after submitting.';
$lang['check']['inquiry_page_ok'] = 'Page "%s" contains a form that stores into tl_wk_inquiry.';
$lang['check']['inquiry_page_form_missing'] = 'Page "%s" contains no form with "store data" → tl_wk_inquiry, so inquiries will not reach the inbox.';
$lang['check']['reader_status_missing'] = 'The redirect page of the calendar, "%s", contains no "W&K booking status" module (free places, deadline, "book now" link).';
$lang['check']['notifications_missing'] = 'No notification assigned. Bookings are stored, but no e-mail is sent.';
$lang['check']['all_ok'] = 'Everything is set up.';
+8
View File
@@ -61,3 +61,11 @@
.wk-sync-matrix td:nth-child(n+2), .wk-sync-matrix th:nth-child(n+2) { white-space: nowrap; }
.wk-sync-matrix th { font-weight: bold; }
.wk-sync-matrix tr:last-child td { border-bottom: 0; }
/* Prüfung der Einrichtung in den W&K-Einstellungen */
.wk-sync-check { margin: 0; padding: 0; list-style: none; }
.wk-sync-check li { margin: .2em 0; padding-left: 1.4em; text-indent: -1.4em; }
.wk-sync-check li::before { display: inline-block; width: 1.4em; text-indent: 0; font-weight: bold; }
.wk-sync-check--ok::before { content: "✓"; }
.wk-sync-check--warn::before { content: "!"; }
.wk-sync-check--error::before { content: "✗"; }
@@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace Mummert\ContaoWkSyncBundle\EventListener\DataContainer;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\PaletteManipulator;
use Contao\StringUtil;
use Doctrine\DBAL\Connection;
use Mummert\ContaoWkSyncBundle\Booking\BookingFields;
use Mummert\ContaoWkSyncBundle\Booking\NotificationRouter;
use Mummert\ContaoWkSyncBundle\Controller\FrontendModule\BookingFormController;
use Mummert\ContaoWkSyncBundle\Controller\FrontendModule\BookingStatusController;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Prüft in den W&K-Einstellungen, ob die Verkettung stimmt: Felder des Anmeldeformulars, Module auf den
* Seiten, Anfrageformular mit Zieltabelle, zugeordnete Benachrichtigungen.
*/
#[AsCallback(table: 'tl_wk_config', target: 'config.onload')]
class SetupCheckListener
{
/**
* Ohne diese Felder ist eine Anmeldung nicht zuzuordnen bzw. nicht zu bestätigen.
*/
private const array ESSENTIAL_FIELDS = ['firstname', 'lastname', 'email'];
public function __construct(
private readonly Connection $connection,
private readonly RequestStack $requestStack,
private readonly TranslatorInterface $translator,
private readonly ContaoFramework $framework,
) {
}
public function __invoke(): void
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request || 'edit' !== $request->query->get('act') || 'wk_config' !== $request->query->get('do')) {
$this->framework->getAdapter(PaletteManipulator::class)->create()->removeField('setupCheck')->applyToPalette('default', 'tl_wk_config');
return;
}
$config = $this->connection->fetchAssociative('SELECT * FROM tl_wk_config WHERE id = ?', [(int) $request->query->get('id')]) ?: [];
$lines = [];
$error = false;
$add = function (string $state, string $key, array $params = []) use (&$lines, &$error): void {
$lines[] = $this->line($state, $key, $params);
$error = $error || 'error' === $state;
};
// Kalender
$calendarId = (int) ($config['calendar'] ?? 0);
if ($calendarId < 1) {
$add('error', 'calendar_missing');
}
// Anmeldeformular: Feldnamen entsprechen den Spalten der Anmeldung
$formId = (int) ($config['bookingForm'] ?? 0);
if ($formId < 1) {
$add('error', 'booking_form_missing');
} else {
$names = $this->connection->fetchFirstColumn("SELECT name FROM tl_form_field WHERE pid = ? AND invisible = 0 AND name != ''", [$formId]);
$missing = array_values(array_diff(BookingFields::COLUMNS, $names));
$missingEssential = array_values(array_intersect(self::ESSENTIAL_FIELDS, $missing));
$formTitle = $this->title('tl_form', $formId);
if ([] !== $missingEssential) {
$add('error', 'booking_form_essential', [$formTitle, implode(', ', $missingEssential)]);
} elseif ([] !== $missing) {
$add('warn', 'booking_form_incomplete', [$formTitle, implode(', ', $missing)]);
} else {
$add('ok', 'booking_form_ok', [$formTitle]);
}
}
// Seite mit dem Anmeldeformular: Modul „W&K-Anmeldeformular“
$bookingPage = (int) ($config['bookingPage'] ?? 0);
if ($bookingPage < 1) {
$add('error', 'booking_page_missing');
} elseif ($this->hasModule($bookingPage, BookingFormController::TYPE)) {
$add('ok', 'booking_page_ok', [$this->title('tl_page', $bookingPage)]);
} else {
$add('error', 'booking_page_module_missing', [$this->title('tl_page', $bookingPage)]);
}
// Bestätigungsseite
if ((int) ($config['confirmationPage'] ?? 0) < 1) {
$add('error', 'confirmation_page_missing');
}
// Anfrageformular (optional): Formular mit Zieltabelle tl_wk_inquiry
$inquiryPage = (int) ($config['inquiryPage'] ?? 0);
if ($inquiryPage > 0) {
$stores = (int) $this->connection->fetchOne(
"SELECT COUNT(*) FROM tl_content c INNER JOIN tl_article a ON a.id = c.pid AND c.ptable = 'tl_article' INNER JOIN tl_form f ON f.id = c.form
WHERE a.pid = ? AND c.type = 'form' AND c.invisible = 0 AND f.storeValues = 1 AND f.targetTable = 'tl_wk_inquiry'",
[$inquiryPage],
);
$add($stores > 0 ? 'ok' : 'error', $stores > 0 ? 'inquiry_page_ok' : 'inquiry_page_form_missing', [$this->title('tl_page', $inquiryPage)]);
}
// Weiterleitungsseite des Kalenders: Modul „W&K-Anmeldestatus“ (Link „Jetzt anmelden“)
$readerPage = $calendarId > 0 ? (int) $this->connection->fetchOne('SELECT jumpTo FROM tl_calendar WHERE id = ?', [$calendarId]) : 0;
if ($readerPage > 0 && !$this->hasModule($readerPage, BookingStatusController::TYPE)) {
$add('warn', 'reader_status_missing', [$this->title('tl_page', $readerPage)]);
}
// Benachrichtigungen
$assigned = (int) ($config['notificationDefault'] ?? 0) > 0;
foreach (NotificationRouter::CONDITIONS as $condition) {
$assigned = $assigned || (int) ($config['notification'.$condition] ?? 0) > 0;
}
if (!$assigned) {
$add('warn', 'notifications_missing');
}
if ([] === $lines) {
$add('ok', 'all_ok');
}
$field = &$GLOBALS['TL_DCA']['tl_wk_config']['fields']['setupCheck'];
$field['eval']['text'] = '<ul class="wk-sync-check">'.implode('', $lines).'</ul>';
$field['eval']['boxClass'] = $error ? 'tl_error' : 'tl_confirm';
}
/**
* @param list<string> $params
*/
private function line(string $state, string $key, array $params = []): string
{
$text = $this->translator->trans('tl_wk_config.check.'.$key, array_map(StringUtil::specialchars(...), $params), 'contao_tl_wk_config');
return \sprintf('<li class="wk-sync-check--%s">%s</li>', $state, $text);
}
/**
* Modul in den Artikeln der Seite oder in einem Seitenlayout.
*/
private function hasModule(int $pageId, string $type): bool
{
$inArticles = (int) $this->connection->fetchOne(
"SELECT COUNT(*) FROM tl_content c INNER JOIN tl_article a ON a.id = c.pid AND c.ptable = 'tl_article' INNER JOIN tl_module m ON m.id = c.module
WHERE a.pid = ? AND c.type = 'module' AND c.invisible = 0 AND m.type = ?",
[$pageId, $type],
);
if ($inArticles > 0) {
return true;
}
$moduleIds = array_map(intval(...), $this->connection->fetchFirstColumn('SELECT id FROM tl_module WHERE type = ?', [$type]));
foreach ($this->connection->fetchFirstColumn('SELECT modules FROM tl_layout') as $modules) {
foreach (StringUtil::deserialize($modules, true) as $entry) {
if (\in_array((int) ($entry['mod'] ?? 0), $moduleIds, true)) {
return true;
}
}
}
return false;
}
private function title(string $table, int $id): string
{
return (string) ($this->connection->fetchOne("SELECT title FROM `$table` WHERE id = ?", [$id]) ?: '#'.$id);
}
}