Initial release
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
/vendor/
|
||||||
|
/var/
|
||||||
|
/.idea/
|
||||||
|
/.vscode/
|
||||||
|
/.DS_Store
|
||||||
|
.DS_Store
|
||||||
|
/composer.lock
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
Create a Contao 5.7 bundle (PHP 8.4)
|
||||||
|
|
||||||
|
Vendor: MummertMedia
|
||||||
|
Bundle: EventManagerBundle
|
||||||
|
Composer name: mummert-media/eventmanager-bundle
|
||||||
|
Type: contao-bundle
|
||||||
|
|
||||||
|
This bundle is internal only.
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
Only implement backend functionality.
|
||||||
|
No frontend logic.
|
||||||
|
No controllers.
|
||||||
|
No services.
|
||||||
|
No access logic.
|
||||||
|
No event listeners.
|
||||||
|
No future planning.
|
||||||
|
Only backend entities and relations.
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
GOAL
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Extend Contao backend with:
|
||||||
|
|
||||||
|
1) New entity: tl_organization named Organisationen, just german translation
|
||||||
|
2) New entity: tl_location named Veranstaltungsorte, just german translation
|
||||||
|
3) Join table: tl_member_organization
|
||||||
|
4) Join table: tl_calendar_events_organization
|
||||||
|
5) Extend tl_calendar_events with:
|
||||||
|
- location_id (exactly one location) with dropdown in Backend (title ASC)
|
||||||
|
- organization assignment (multiple via join table)
|
||||||
|
6) Extend tl_member so that:
|
||||||
|
- A member can belong to multiple organizations
|
||||||
|
7) Extend tl_organization so that:
|
||||||
|
- Multiple members can be assigned to it
|
||||||
|
|
||||||
|
Everything manageable via Contao backend only.
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
DATABASE TABLES
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Use Contao DCA SQL definitions (DBAL 4 compatible).
|
||||||
|
Do NOT use serialized fields.
|
||||||
|
Do NOT use string length values like "255".
|
||||||
|
Use integer values.
|
||||||
|
|
||||||
|
------------------------------------
|
||||||
|
tl_organization + add german translation
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
id int unsigned auto_increment
|
||||||
|
pid int unsigned default 0
|
||||||
|
tstamp int unsigned default 0
|
||||||
|
dateAdded int unsigned default 0
|
||||||
|
title varchar(255)
|
||||||
|
alias varchar(128)
|
||||||
|
logo binary(16) nullable
|
||||||
|
street varchar(255)
|
||||||
|
street2 varchar(255)
|
||||||
|
postal varchar(255)
|
||||||
|
city varchar(255)
|
||||||
|
state varchar(255)
|
||||||
|
country varchar(2)
|
||||||
|
phone varchar(64)
|
||||||
|
email varchar(255)
|
||||||
|
website varchar(255)
|
||||||
|
lat decimal(10,8)
|
||||||
|
lng decimal(11,8)
|
||||||
|
description text nullable
|
||||||
|
published char(1)
|
||||||
|
isExternal char(1)
|
||||||
|
type varchar(32)
|
||||||
|
|
||||||
|
------------------------------------
|
||||||
|
tl_location + add german translation
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
id int unsigned auto_increment
|
||||||
|
tstamp int unsigned default 0
|
||||||
|
dateAdded int unsigned default 0
|
||||||
|
title varchar(255)
|
||||||
|
alias varchar(128)
|
||||||
|
description text nullable
|
||||||
|
street varchar(255)
|
||||||
|
street2 varchar(255)
|
||||||
|
postal varchar(255)
|
||||||
|
city varchar(255)
|
||||||
|
state varchar(255)
|
||||||
|
country varchar(2) default 'de'
|
||||||
|
lat decimal(10,8)
|
||||||
|
lng decimal(11,8)
|
||||||
|
image binary(16) nullable
|
||||||
|
published char(1)
|
||||||
|
|
||||||
|
------------------------------------
|
||||||
|
tl_member_organization
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
id int unsigned auto_increment
|
||||||
|
tstamp int unsigned default 0
|
||||||
|
member_id int unsigned
|
||||||
|
organization_id int unsigned
|
||||||
|
|
||||||
|
------------------------------------
|
||||||
|
tl_calendar_events_organization
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
id int unsigned auto_increment
|
||||||
|
tstamp int unsigned default 0
|
||||||
|
event_id int unsigned
|
||||||
|
organization_id int unsigned
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
DCA REQUIREMENTS
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
1) tl_organization:
|
||||||
|
- Backend list view
|
||||||
|
- Editable fields
|
||||||
|
- Published toggle
|
||||||
|
- Alias field
|
||||||
|
- Type as select field with static options:
|
||||||
|
accommodation, shopping, culture
|
||||||
|
- Add multi-relation field for members
|
||||||
|
(store relation in tl_member_organization)
|
||||||
|
|
||||||
|
2) tl_location:
|
||||||
|
- Backend list view
|
||||||
|
- Editable fields
|
||||||
|
- Published toggle
|
||||||
|
- Alias field
|
||||||
|
|
||||||
|
3) tl_calendar_events:
|
||||||
|
- Add field location_id (select, mandatory)
|
||||||
|
foreignKey: tl_location.title
|
||||||
|
- Add multi-organization relation
|
||||||
|
(store relation in tl_calendar_events_organization)
|
||||||
|
|
||||||
|
4) tl_member:
|
||||||
|
- Add multi-organization relation
|
||||||
|
(store relation in tl_member_organization)
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
RELATION HANDLING
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Use proper many-to-many handling via join tables.
|
||||||
|
Use relation definitions in DCA.
|
||||||
|
Do NOT use serialized arrays.
|
||||||
|
Do NOT store IDs as comma-separated strings.
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
BACKEND INTEGRATION
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Register backend modules for:
|
||||||
|
|
||||||
|
- Organisationen
|
||||||
|
- Veranstaltungsorte
|
||||||
|
|
||||||
|
Under backend group content
|
||||||
|
|
||||||
|
|
||||||
|
Use standard Contao backend module registration.
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
PROJECT STRUCTURE
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Generate:
|
||||||
|
|
||||||
|
src/
|
||||||
|
MummertMediaEventManagerBundle.php
|
||||||
|
DependencyInjection/
|
||||||
|
MummertMediaEventManagerExtension.php
|
||||||
|
Contao/
|
||||||
|
Manager/
|
||||||
|
Plugin.php
|
||||||
|
|
||||||
|
contao/
|
||||||
|
dca/
|
||||||
|
tl_organization.php
|
||||||
|
tl_location.php
|
||||||
|
tl_member.php
|
||||||
|
tl_calendar_events.php
|
||||||
|
config/
|
||||||
|
config.php
|
||||||
|
|
||||||
|
config/
|
||||||
|
services.yaml
|
||||||
|
|
||||||
|
composer.json
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
QUALITY RULES
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
- declare(strict_types=1);
|
||||||
|
- PHP 8.4
|
||||||
|
- DBAL 4 compatible SQL
|
||||||
|
- no deprecated Contao 4 syntax
|
||||||
|
- no frontend logic
|
||||||
|
- no controllers
|
||||||
|
- no services
|
||||||
|
- no placeholders
|
||||||
|
- complete working DCA
|
||||||
|
- ready to install via path repository
|
||||||
|
|
||||||
|
====================================================
|
||||||
|
OUTPUT
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
Generate full file contents for all required files.
|
||||||
|
Code must be production-ready.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright (c) Mummert Media
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This package is proprietary software.
|
||||||
|
No part of this package may be copied, modified, distributed, sublicensed,
|
||||||
|
or used in any form without prior written permission of the copyright holder.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Eventmanager Bundle
|
||||||
|
|
||||||
|
Contao 5 Bundle für Organisations-, Veranstaltungsort- und Event-Verwaltung inkl. Frontend-Module.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- PHP `^8.4`
|
||||||
|
- Contao `^5.7`
|
||||||
|
|
||||||
|
## Installation (lokal via Composer)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require mummert-media/eventmanager-bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation im Contao Manager (VCS)
|
||||||
|
|
||||||
|
1. **Contao Manager** → **Composer** → **Repositories** → **Repository hinzufügen**
|
||||||
|
2. Typ: `vcs`
|
||||||
|
3. URL: `https://github.com/<owner>/eventmanager-bundle`
|
||||||
|
4. Paket hinzufügen: `mummert-media/eventmanager-bundle`
|
||||||
|
5. Bei Bedarf Version `dev-main` oder ein Tag (z. B. `1.0.0`) wählen
|
||||||
|
6. Änderungen anwenden
|
||||||
|
|
||||||
|
## Release / Versionierung
|
||||||
|
|
||||||
|
SemVer-Tags verwenden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.0.0
|
||||||
|
git push origin v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial release"
|
||||||
|
git branch -M main
|
||||||
|
git remote add origin git@github.com:<owner>/eventmanager-bundle.git
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional: Packagist
|
||||||
|
|
||||||
|
1. Repository auf Packagist einreichen
|
||||||
|
2. GitHub Service Hook / Auto-Update aktivieren
|
||||||
|
3. Neue Versionen über Git-Tags veröffentlichen
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "mummert-media/eventmanager-bundle",
|
||||||
|
"description": "Internal Contao backend bundle for organizations, locations and event/member assignments.",
|
||||||
|
"type": "contao-bundle",
|
||||||
|
"license": "proprietary",
|
||||||
|
"keywords": [
|
||||||
|
"contao",
|
||||||
|
"contao-bundle",
|
||||||
|
"eventmanager"
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.4",
|
||||||
|
"contao/core-bundle": "^5.7",
|
||||||
|
"contao/manager-plugin": "^2.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"MummertMedia\\EventManagerBundle\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"contao-manager-plugin": "MummertMedia\\EventManagerBundle\\Contao\\Manager\\Plugin"
|
||||||
|
},
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
|
MummertMedia\EventManagerBundle\:
|
||||||
|
resource: ../src/
|
||||||
|
exclude:
|
||||||
|
- ../src/DependencyInjection/
|
||||||
|
- ../src/Contao/Manager/
|
||||||
|
- ../src/MummertMediaEventManagerBundle.php
|
||||||
|
|
||||||
|
MummertMedia\EventManagerBundle\EventListener\DataContainer\SetDateAddedCallback:
|
||||||
|
tags:
|
||||||
|
- { name: contao.callback, table: tl_organization, target: config.onbeforesubmit, method: onBeforeSubmit }
|
||||||
|
- { name: contao.callback, table: tl_location, target: config.onbeforesubmit, method: onBeforeSubmit }
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_MODELS']['tl_organization'] = MummertMedia\EventManagerBundle\Model\OrganizationModel::class;
|
||||||
|
$GLOBALS['TL_MODELS']['tl_location'] = MummertMedia\EventManagerBundle\Model\LocationModel::class;
|
||||||
|
|
||||||
|
$GLOBALS['BE_MOD']['content']['eventmanager_organisationen'] = [
|
||||||
|
'tables' => ['tl_organization'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['BE_MOD']['content']['eventmanager_veranstaltungsorte'] = [
|
||||||
|
'tables' => ['tl_location'],
|
||||||
|
];
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Contao\CoreBundle\DataContainer\PaletteManipulator;
|
||||||
|
use Contao\DataContainer;
|
||||||
|
use Contao\Database;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
|
||||||
|
unset($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['location']);
|
||||||
|
unset($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['address']);
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['config']['onload_callback'][] = static function (): void {
|
||||||
|
if (!isset($GLOBALS['TL_DCA']['tl_calendar_events']['palettes']) || !is_array($GLOBALS['TL_DCA']['tl_calendar_events']['palettes'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys($GLOBALS['TL_DCA']['tl_calendar_events']['palettes']) as $paletteName) {
|
||||||
|
if ($paletteName === '__selector__') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaletteManipulator::create()
|
||||||
|
->addLegend('organization_legend', 'details_legend', PaletteManipulator::POSITION_AFTER)
|
||||||
|
->addField(['location_id', 'type', 'organizations'], 'organization_legend', PaletteManipulator::POSITION_APPEND)
|
||||||
|
->applyToPalette((string) $paletteName, 'tl_calendar_events');
|
||||||
|
|
||||||
|
PaletteManipulator::create()
|
||||||
|
->addField('photographer', 'image_legend', PaletteManipulator::POSITION_APPEND)
|
||||||
|
->applyToPalette((string) $paletteName, 'tl_calendar_events');
|
||||||
|
|
||||||
|
PaletteManipulator::create()
|
||||||
|
->addField(['source', 'url', 'target'], 'details_legend', PaletteManipulator::POSITION_APPEND)
|
||||||
|
->addField(['termsAccepted', 'isSoldOut', 'isCanceled'], 'publish_legend', PaletteManipulator::POSITION_APPEND)
|
||||||
|
->applyToPalette((string) $paletteName, 'tl_calendar_events');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['config']['onbeforesubmit_callback'][] = static function (array $values): array {
|
||||||
|
unset($values['organizations']);
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
};
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['location_id'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['location_id'],
|
||||||
|
'exclude' => true,
|
||||||
|
'filter' => true,
|
||||||
|
'sorting' => true,
|
||||||
|
'inputType' => 'select',
|
||||||
|
'foreignKey' => 'tl_location.title',
|
||||||
|
'options_callback' => static function (): array {
|
||||||
|
$result = Database::getInstance()
|
||||||
|
->query('SELECT id, title FROM tl_location ORDER BY title ASC');
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
foreach ($result->fetchAllAssoc() as $row) {
|
||||||
|
$options[(int) $row['id']] = (string) $row['title'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
},
|
||||||
|
'eval' => ['mandatory' => true, 'includeBlankOption' => true, 'chosen' => true, 'tl_class' => 'w50'],
|
||||||
|
'relation' => [
|
||||||
|
'type' => 'hasOne',
|
||||||
|
'load' => 'eager',
|
||||||
|
'table' => 'tl_location',
|
||||||
|
'field' => 'id',
|
||||||
|
],
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['type'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['type'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'select',
|
||||||
|
'options' => ['accommodation', 'shopping', 'culture'],
|
||||||
|
'reference' => &$GLOBALS['TL_LANG']['tl_calendar_events']['type_options'],
|
||||||
|
'eval' => ['multiple' => true, 'chosen' => true, 'includeBlankOption' => false, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'blob', 'notnull' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['organizations'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['organizations'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'select',
|
||||||
|
'foreignKey' => 'tl_organization.title',
|
||||||
|
'eval' => ['multiple' => true, 'chosen' => true, 'tl_class' => 'clr'],
|
||||||
|
'relation' => [
|
||||||
|
'type' => 'hasMany',
|
||||||
|
'load' => 'lazy',
|
||||||
|
'table' => 'tl_organization',
|
||||||
|
'field' => 'id',
|
||||||
|
],
|
||||||
|
'load_callback' => [
|
||||||
|
static function ($value, DataContainer $dc): array {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Database::getInstance()
|
||||||
|
->prepare('SELECT organization_id FROM tl_calendar_events_organization WHERE event_id=? ORDER BY organization_id')
|
||||||
|
->execute($dc->id);
|
||||||
|
|
||||||
|
return array_map(static fn (array $row): int => (int) $row['organization_id'], $result->fetchAllAssoc());
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'save_callback' => [
|
||||||
|
static function ($value, DataContainer $dc): array {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventId = (int) $dc->id;
|
||||||
|
$organizationIds = array_values(array_unique(array_map('intval', StringUtil::deserialize($value, true))));
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$time = time();
|
||||||
|
|
||||||
|
$db->prepare('DELETE FROM tl_calendar_events_organization WHERE event_id=?')
|
||||||
|
->execute($eventId);
|
||||||
|
|
||||||
|
foreach ($organizationIds as $organizationId) {
|
||||||
|
$db->prepare('INSERT INTO tl_calendar_events_organization (tstamp, event_id, organization_id) VALUES (?, ?, ?)')
|
||||||
|
->execute($time, $eventId, $organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $organizationIds;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['photographer'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['photographer'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['isSoldOut'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['isSoldOut'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'checkbox',
|
||||||
|
'eval' => ['tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['isCanceled'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['isCanceled'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'checkbox',
|
||||||
|
'eval' => ['tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['termsAccepted'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_calendar_events']['termsAccepted'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'checkbox',
|
||||||
|
'eval' => ['tl_class' => 'clr'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['source']) && is_array($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['source'])) {
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['source']['eval'] ??= [];
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['source']['eval']['tl_class'] = 'w50';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['target']) && is_array($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['target'])) {
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['target']['eval'] ??= [];
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['target']['eval']['tl_class'] = 'w50';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['url']) && is_array($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['url'])) {
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['url']['eval'] ??= [];
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['url']['eval']['tl_class'] = 'clr';
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['url']['eval']['mandatory'] = false;
|
||||||
|
unset($GLOBALS['TL_DCA']['tl_calendar_events']['fields']['url']['eval']['rgxp']);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Contao\DC_Table;
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_calendar_events_organization'] = [
|
||||||
|
'config' => [
|
||||||
|
'dataContainer' => DC_Table::class,
|
||||||
|
'sql' => [
|
||||||
|
'keys' => [
|
||||||
|
'id' => 'primary',
|
||||||
|
'event_id' => 'index',
|
||||||
|
'organization_id' => 'index',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true, 'notnull' => true],
|
||||||
|
],
|
||||||
|
'tstamp' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'notnull' => true],
|
||||||
|
],
|
||||||
|
'event_id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'notnull' => true],
|
||||||
|
],
|
||||||
|
'organization_id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'notnull' => true],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Contao\DataContainer;
|
||||||
|
use Contao\Database;
|
||||||
|
use Contao\DC_Table;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_location'] = [
|
||||||
|
'config' => [
|
||||||
|
'dataContainer' => DC_Table::class,
|
||||||
|
'enableVersioning' => true,
|
||||||
|
'sql' => [
|
||||||
|
'keys' => [
|
||||||
|
'id' => 'primary',
|
||||||
|
'alias' => 'index',
|
||||||
|
'published' => 'index',
|
||||||
|
'title' => 'index',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'list' => [
|
||||||
|
'sorting' => [
|
||||||
|
'mode' => 1,
|
||||||
|
'fields' => ['title'],
|
||||||
|
'flag' => 1,
|
||||||
|
'panelLayout' => 'search,limit',
|
||||||
|
],
|
||||||
|
'label' => [
|
||||||
|
'fields' => ['title', 'city'],
|
||||||
|
'format' => '%s (%s)',
|
||||||
|
],
|
||||||
|
'global_operations' => [
|
||||||
|
'all' => [
|
||||||
|
'href' => 'act=select',
|
||||||
|
'class' => 'header_edit_all',
|
||||||
|
'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'operations' => [
|
||||||
|
'edit' => [
|
||||||
|
'href' => 'act=edit',
|
||||||
|
'icon' => 'edit.svg',
|
||||||
|
],
|
||||||
|
'copy' => [
|
||||||
|
'href' => 'act=copy',
|
||||||
|
'icon' => 'copy.svg',
|
||||||
|
],
|
||||||
|
'delete' => [
|
||||||
|
'href' => 'act=delete',
|
||||||
|
'icon' => 'delete.svg',
|
||||||
|
'attributes' => 'onclick="if(!confirm(\'' . (string) ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? '') . '\'))return false;Backend.getScrollOffset()"',
|
||||||
|
],
|
||||||
|
'toggle' => [
|
||||||
|
'href' => 'act=toggle&field=published',
|
||||||
|
'icon' => 'visible.svg',
|
||||||
|
],
|
||||||
|
'show' => [
|
||||||
|
'href' => 'act=show',
|
||||||
|
'icon' => 'show.svg',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'palettes' => [
|
||||||
|
'__selector__' => [],
|
||||||
|
'default' => '{title_legend},title,alias,image;{address_legend},street,street2,postal,city,state,country;{geo_legend},lat,lng;{description_legend},description;{publish_legend},published',
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true],
|
||||||
|
],
|
||||||
|
'tstamp' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
],
|
||||||
|
'dateAdded' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
],
|
||||||
|
'title' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['title'],
|
||||||
|
'exclude' => true,
|
||||||
|
'search' => true,
|
||||||
|
'sorting' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['mandatory' => true, 'maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'alias' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['alias'],
|
||||||
|
'exclude' => true,
|
||||||
|
'search' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['rgxp' => 'alias', 'unique' => true, 'maxlength' => 128, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 128, 'default' => ''],
|
||||||
|
],
|
||||||
|
'description' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['description'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'textarea',
|
||||||
|
'eval' => ['rte' => 'tinyMCE', 'tl_class' => 'clr'],
|
||||||
|
'sql' => ['type' => 'text', 'notnull' => false],
|
||||||
|
],
|
||||||
|
'street' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['street'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'street2' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['street2'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'postal' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['postal'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'city' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['city'],
|
||||||
|
'exclude' => true,
|
||||||
|
'search' => true,
|
||||||
|
'sorting' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'state' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['state'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'country' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['country'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 2, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 2, 'default' => 'de'],
|
||||||
|
],
|
||||||
|
'lat' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['lat'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 10, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'decimal', 'precision' => 10, 'scale' => 8, 'default' => '0.00000000'],
|
||||||
|
],
|
||||||
|
'lng' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['lng'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 11, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'decimal', 'precision' => 11, 'scale' => 8, 'default' => '0.00000000'],
|
||||||
|
],
|
||||||
|
'image' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['image'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'fileTree',
|
||||||
|
'eval' => ['filesOnly' => true, 'fieldType' => 'radio', 'tl_class' => 'clr'],
|
||||||
|
'sql' => ['type' => 'binary', 'length' => 16, 'notnull' => false],
|
||||||
|
],
|
||||||
|
'published' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_location']['published'],
|
||||||
|
'exclude' => true,
|
||||||
|
'filter' => true,
|
||||||
|
'toggle' => true,
|
||||||
|
'inputType' => 'checkbox',
|
||||||
|
'eval' => ['doNotCopy' => true],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_location']['fields']['alias']['save_callback'][] = static function (string $value, DataContainer $dc): string {
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = StringUtil::generateAlias($dc->activeRecord?->title ?? '');
|
||||||
|
|
||||||
|
$exists = Database::getInstance()
|
||||||
|
->prepare('SELECT id FROM tl_location WHERE alias=? AND id!=?')
|
||||||
|
->execute($value, (int) ($dc->id ?? 0));
|
||||||
|
|
||||||
|
if ($exists->numRows > 0) {
|
||||||
|
throw new \RuntimeException(sprintf((string) ($GLOBALS['TL_LANG']['tl_location']['aliasExists'] ?? '%s'), $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Contao\CoreBundle\DataContainer\PaletteManipulator;
|
||||||
|
use Contao\DataContainer;
|
||||||
|
use Contao\Database;
|
||||||
|
use Contao\Input;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
|
||||||
|
if (isset($GLOBALS['TL_DCA']['tl_member']['palettes']) && is_array($GLOBALS['TL_DCA']['tl_member']['palettes'])) {
|
||||||
|
foreach (array_keys($GLOBALS['TL_DCA']['tl_member']['palettes']) as $paletteName) {
|
||||||
|
if ($paletteName === '__selector__') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaletteManipulator::create()
|
||||||
|
->addLegend('organization_legend', 'contact_legend', PaletteManipulator::POSITION_AFTER)
|
||||||
|
->addField('organizations', 'organization_legend', PaletteManipulator::POSITION_APPEND)
|
||||||
|
->applyToPalette((string) $paletteName, 'tl_member');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_member']['config']['onbeforesubmit_callback'][] = static function (array $values): array {
|
||||||
|
unset($values['organizations']);
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
};
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_member']['config']['onsubmit_callback'][] = static function (DataContainer $dc): void {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$postedValues = Input::post('organizations');
|
||||||
|
|
||||||
|
if (null === $postedValues) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$organizationIds = array_values(array_unique(array_map('intval', is_array($postedValues) ? $postedValues : [$postedValues])));
|
||||||
|
$memberId = (int) $dc->id;
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$time = time();
|
||||||
|
|
||||||
|
$db->prepare('DELETE FROM tl_member_organization WHERE member_id=?')
|
||||||
|
->execute($memberId);
|
||||||
|
|
||||||
|
foreach ($organizationIds as $organizationId) {
|
||||||
|
$db->prepare('INSERT INTO tl_member_organization (tstamp, member_id, organization_id) VALUES (?, ?, ?)')
|
||||||
|
->execute($time, $memberId, $organizationId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_member']['fields']['organizations'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_member']['organizations'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'select',
|
||||||
|
'foreignKey' => 'tl_organization.title',
|
||||||
|
'eval' => ['multiple' => true, 'chosen' => true, 'tl_class' => 'clr'],
|
||||||
|
'relation' => [
|
||||||
|
'type' => 'hasMany',
|
||||||
|
'load' => 'lazy',
|
||||||
|
'table' => 'tl_organization',
|
||||||
|
'field' => 'id',
|
||||||
|
],
|
||||||
|
'load_callback' => [
|
||||||
|
static function ($value, DataContainer $dc): array {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Database::getInstance()
|
||||||
|
->prepare('SELECT organization_id FROM tl_member_organization WHERE member_id=? ORDER BY organization_id')
|
||||||
|
->execute($dc->id);
|
||||||
|
|
||||||
|
return array_map(static fn (array $row): int => (int) $row['organization_id'], $result->fetchAllAssoc());
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'save_callback' => [
|
||||||
|
static function ($value, DataContainer $dc): array {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$memberId = (int) $dc->id;
|
||||||
|
$organizationIds = array_values(array_unique(array_map('intval', StringUtil::deserialize($value, true))));
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$time = time();
|
||||||
|
|
||||||
|
$db->prepare('DELETE FROM tl_member_organization WHERE member_id=?')
|
||||||
|
->execute($memberId);
|
||||||
|
|
||||||
|
foreach ($organizationIds as $organizationId) {
|
||||||
|
$db->prepare('INSERT INTO tl_member_organization (tstamp, member_id, organization_id) VALUES (?, ?, ?)')
|
||||||
|
->execute($time, $memberId, $organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $organizationIds;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Contao\DC_Table;
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_member_organization'] = [
|
||||||
|
'config' => [
|
||||||
|
'dataContainer' => DC_Table::class,
|
||||||
|
'sql' => [
|
||||||
|
'keys' => [
|
||||||
|
'id' => 'primary',
|
||||||
|
'member_id' => 'index',
|
||||||
|
'organization_id' => 'index',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true, 'notnull' => true],
|
||||||
|
],
|
||||||
|
'tstamp' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'notnull' => true],
|
||||||
|
],
|
||||||
|
'member_id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'notnull' => true],
|
||||||
|
],
|
||||||
|
'organization_id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'notnull' => true],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['palettes']['member_organizations'] = '{title_legend},name,headline,type;{eventmanager_legend},editPage;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['palettes']['member_events'] = '{title_legend},name,headline,type;{eventmanager_legend},editPage;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['palettes']['organization_edit'] = '{title_legend},name,headline,type;{eventmanager_legend},listPage,logoFolder;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['palettes']['event_edit'] = '{title_legend},name,headline,type;{eventmanager_legend},listPage,eventFolder,termsPage,frontendAuthorId,frontendArchiveId;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['editPage'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['editPage'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'pageTree',
|
||||||
|
'eval' => ['fieldType' => 'radio', 'mandatory' => true, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['listPage'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['listPage'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'pageTree',
|
||||||
|
'eval' => ['fieldType' => 'radio', 'mandatory' => true, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['logoFolder'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['logoFolder'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'fileTree',
|
||||||
|
'eval' => ['fieldType' => 'radio', 'files' => false, 'mandatory' => true, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'binary', 'length' => 16, 'notnull' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['eventFolder'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['eventFolder'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'fileTree',
|
||||||
|
'eval' => ['fieldType' => 'radio', 'files' => false, 'mandatory' => true, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'binary', 'length' => 16, 'notnull' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['termsPage'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['termsPage'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'pageTree',
|
||||||
|
'eval' => ['fieldType' => 'radio', 'mandatory' => false, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['frontendAuthorId'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['frontendAuthorId'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['mandatory' => true, 'rgxp' => 'digit', 'maxlength' => 10, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_module']['fields']['frontendArchiveId'] = [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_module']['frontendArchiveId'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['mandatory' => true, 'rgxp' => 'digit', 'maxlength' => 10, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
];
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Contao\DataContainer;
|
||||||
|
use Contao\Database;
|
||||||
|
use Contao\DC_Table;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_organization'] = [
|
||||||
|
'config' => [
|
||||||
|
'dataContainer' => DC_Table::class,
|
||||||
|
'enableVersioning' => true,
|
||||||
|
'onbeforesubmit_callback' => [
|
||||||
|
static function (array $values): array {
|
||||||
|
unset($values['members']);
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'sql' => [
|
||||||
|
'keys' => [
|
||||||
|
'id' => 'primary',
|
||||||
|
'alias' => 'index',
|
||||||
|
'published' => 'index',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'list' => [
|
||||||
|
'sorting' => [
|
||||||
|
'mode' => 1,
|
||||||
|
'fields' => ['title'],
|
||||||
|
'flag' => 1,
|
||||||
|
'panelLayout' => 'search,limit',
|
||||||
|
],
|
||||||
|
'label' => [
|
||||||
|
'fields' => ['title', 'city'],
|
||||||
|
'format' => '%s (%s)',
|
||||||
|
],
|
||||||
|
'global_operations' => [
|
||||||
|
'all' => [
|
||||||
|
'href' => 'act=select',
|
||||||
|
'class' => 'header_edit_all',
|
||||||
|
'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'operations' => [
|
||||||
|
'edit' => [
|
||||||
|
'href' => 'act=edit',
|
||||||
|
'icon' => 'edit.svg',
|
||||||
|
],
|
||||||
|
'copy' => [
|
||||||
|
'href' => 'act=copy',
|
||||||
|
'icon' => 'copy.svg',
|
||||||
|
],
|
||||||
|
'delete' => [
|
||||||
|
'href' => 'act=delete',
|
||||||
|
'icon' => 'delete.svg',
|
||||||
|
'attributes' => 'onclick="if(!confirm(\'' . (string) ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? '') . '\'))return false;Backend.getScrollOffset()"',
|
||||||
|
],
|
||||||
|
'toggle' => [
|
||||||
|
'href' => 'act=toggle&field=published',
|
||||||
|
'icon' => 'visible.svg',
|
||||||
|
],
|
||||||
|
'show' => [
|
||||||
|
'href' => 'act=show',
|
||||||
|
'icon' => 'show.svg',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'palettes' => [
|
||||||
|
'__selector__' => [],
|
||||||
|
'default' => '{title_legend},title,alias,type,isExternal,logo;{address_legend},street,street2,postal,city,state,country;{contact_legend},phone,email,website;{geo_legend},lat,lng;{description_legend},description;{relation_legend},members;{publish_legend},published',
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
'id' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'autoincrement' => true],
|
||||||
|
],
|
||||||
|
'pid' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
],
|
||||||
|
'tstamp' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
],
|
||||||
|
'dateAdded' => [
|
||||||
|
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
|
||||||
|
],
|
||||||
|
'title' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['title'],
|
||||||
|
'exclude' => true,
|
||||||
|
'search' => true,
|
||||||
|
'sorting' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['mandatory' => true, 'maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'alias' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['alias'],
|
||||||
|
'exclude' => true,
|
||||||
|
'search' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['rgxp' => 'alias', 'unique' => true, 'maxlength' => 128, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 128, 'default' => ''],
|
||||||
|
],
|
||||||
|
'logo' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['logo'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'fileTree',
|
||||||
|
'eval' => ['filesOnly' => true, 'fieldType' => 'radio', 'tl_class' => 'clr'],
|
||||||
|
'sql' => ['type' => 'binary', 'length' => 16, 'notnull' => false],
|
||||||
|
],
|
||||||
|
'street' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['street'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'street2' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['street2'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'postal' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['postal'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'city' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['city'],
|
||||||
|
'exclude' => true,
|
||||||
|
'search' => true,
|
||||||
|
'sorting' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'state' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['state'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'country' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['country'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 2, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 2, 'default' => ''],
|
||||||
|
],
|
||||||
|
'phone' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['phone'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 64, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 64, 'default' => ''],
|
||||||
|
],
|
||||||
|
'email' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['email'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['rgxp' => 'email', 'maxlength' => 255, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'website' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['website'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['rgxp' => 'url', 'maxlength' => 255, 'tl_class' => 'clr'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 255, 'default' => ''],
|
||||||
|
],
|
||||||
|
'lat' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['lat'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 10, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'decimal', 'precision' => 10, 'scale' => 8, 'default' => '0.00000000'],
|
||||||
|
],
|
||||||
|
'lng' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['lng'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'text',
|
||||||
|
'eval' => ['maxlength' => 11, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'decimal', 'precision' => 11, 'scale' => 8, 'default' => '0.00000000'],
|
||||||
|
],
|
||||||
|
'description' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['description'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'textarea',
|
||||||
|
'eval' => ['rte' => 'tinyMCE', 'tl_class' => 'clr'],
|
||||||
|
'sql' => ['type' => 'text', 'notnull' => false],
|
||||||
|
],
|
||||||
|
'published' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['published'],
|
||||||
|
'exclude' => true,
|
||||||
|
'filter' => true,
|
||||||
|
'toggle' => true,
|
||||||
|
'inputType' => 'checkbox',
|
||||||
|
'eval' => ['doNotCopy' => true],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
|
||||||
|
],
|
||||||
|
'isExternal' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['isExternal'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'checkbox',
|
||||||
|
'eval' => ['tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
|
||||||
|
],
|
||||||
|
'type' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['type'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'select',
|
||||||
|
'options' => ['accommodation', 'shopping', 'culture'],
|
||||||
|
'reference' => &$GLOBALS['TL_LANG']['tl_organization']['type_options'],
|
||||||
|
'eval' => ['multiple' => true, 'chosen' => true, 'includeBlankOption' => false, 'tl_class' => 'w50'],
|
||||||
|
'sql' => ['type' => 'blob', 'notnull' => false],
|
||||||
|
],
|
||||||
|
'members' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_organization']['members'],
|
||||||
|
'exclude' => true,
|
||||||
|
'inputType' => 'select',
|
||||||
|
'options_callback' => static function (): array {
|
||||||
|
$result = Database::getInstance()
|
||||||
|
->query('SELECT id, firstname, lastname FROM tl_member ORDER BY lastname ASC, firstname ASC');
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
foreach ($result->fetchAllAssoc() as $row) {
|
||||||
|
$name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? ''));
|
||||||
|
$options[(int) $row['id']] = $name !== ''
|
||||||
|
? $name
|
||||||
|
: sprintf((string) ($GLOBALS['TL_LANG']['tl_organization']['memberFallback'] ?? '%s'), $row['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
},
|
||||||
|
'eval' => ['multiple' => true, 'chosen' => true, 'tl_class' => 'clr'],
|
||||||
|
'relation' => [
|
||||||
|
'type' => 'hasMany',
|
||||||
|
'load' => 'lazy',
|
||||||
|
'table' => 'tl_member',
|
||||||
|
'field' => 'id',
|
||||||
|
],
|
||||||
|
'load_callback' => [
|
||||||
|
static function ($value, DataContainer $dc): array {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Database::getInstance()
|
||||||
|
->prepare('SELECT member_id FROM tl_member_organization WHERE organization_id=? ORDER BY member_id')
|
||||||
|
->execute($dc->id);
|
||||||
|
|
||||||
|
return array_map(static fn (array $row): int => (int) $row['member_id'], $result->fetchAllAssoc());
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'save_callback' => [
|
||||||
|
static function ($value, DataContainer $dc): array {
|
||||||
|
if (!$dc->id) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$organizationId = (int) $dc->id;
|
||||||
|
$memberIds = array_values(array_unique(array_map('intval', StringUtil::deserialize($value, true))));
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$time = time();
|
||||||
|
|
||||||
|
$db->prepare('DELETE FROM tl_member_organization WHERE organization_id=?')
|
||||||
|
->execute($organizationId);
|
||||||
|
|
||||||
|
foreach ($memberIds as $memberId) {
|
||||||
|
$db->prepare('INSERT INTO tl_member_organization (tstamp, member_id, organization_id) VALUES (?, ?, ?)')
|
||||||
|
->execute($time, $memberId, $organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $memberIds;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$GLOBALS['TL_DCA']['tl_organization']['fields']['alias']['save_callback'][] = static function (string $value, DataContainer $dc): string {
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = StringUtil::generateAlias($dc->activeRecord?->title ?? '');
|
||||||
|
|
||||||
|
$exists = Database::getInstance()
|
||||||
|
->prepare('SELECT id FROM tl_organization WHERE alias=? AND id!=?')
|
||||||
|
->execute($value, (int) ($dc->id ?? 0));
|
||||||
|
|
||||||
|
if ($exists->numRows > 0) {
|
||||||
|
throw new \RuntimeException(sprintf((string) ($GLOBALS['TL_LANG']['tl_organization']['aliasExists'] ?? '%s'), $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['MOD']['eventmanager_organisationen'] = ['Organisationen', 'Organisationen verwalten'];
|
||||||
|
$GLOBALS['TL_LANG']['MOD']['eventmanager_veranstaltungsorte'] = ['Veranstaltungsorte', 'Veranstaltungsorte verwalten'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['eventmanager'] = 'Event-Manager';
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['member_organizations'] = ['Meine Organisationen', 'Listet Organisationen des eingeloggten Mitglieds auf.'];
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['organization_edit'] = ['Organisation bearbeiten', 'Bearbeitungsformular für eine Organisation.'];
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['member_events'] = ['Meine Veranstaltungen', 'Listet Veranstaltungen der zugeordneten Organisationen auf.'];
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['event_edit'] = ['Veranstaltung bearbeiten', 'Bearbeitungsformular für eine Veranstaltung.'];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['organization_legend'] = 'Organisationen und Ort';
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['location_id'] = ['Veranstaltungsort', 'Zugeordneter Veranstaltungsort'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type'] = ['Typ', 'Mehrere Typen auswählbar.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['organizations'] = ['Organisationen', 'Zugeordnete Organisationen'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['photographer'] = ['Urheber/Fotograf', 'Die Angabe des Urhebers ist notwendig. Ihnen muss eine Genehmigung zur Verwendung des Bildes vorliegen.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['isSoldOut'] = ['Ausverkauft', 'Diese Veranstaltung ist ausverkauft.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['isCanceled'] = ['Abgesagt', 'Diese Veranstaltung wurde abgesagt.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['termsAccepted'] = ['Nutzungsbedingungen akzeptiert', 'Die Nutzungsbedingungen wurden akzeptiert.'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type_options']['accommodation'] = 'Unterkunft';
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type_options']['shopping'] = 'Shopping';
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type_options']['culture'] = 'Kultur';
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['title'] = ['Titel', 'Name des Veranstaltungsortes'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['alias'] = ['Alias', 'Eindeutiger Alias des Veranstaltungsortes'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['description'] = ['Beschreibung', 'Beschreibung'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['street'] = ['Straße', 'Straße'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['street2'] = ['Adresszusatz', 'Adresszusatz'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['postal'] = ['PLZ', 'Postleitzahl'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['city'] = ['Ort', 'Ort'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['state'] = ['Bundesland', 'Bundesland'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['country'] = ['Land', 'Land'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['lat'] = ['Breitengrad', 'Breitengrad'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['lng'] = ['Längengrad', 'Längengrad'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['image'] = ['Bild', 'Bild-Datei'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['published'] = ['Veröffentlicht', 'Veranstaltungsort veröffentlichen'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['title_legend'] = 'Titel';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['address_legend'] = 'Adresse';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['geo_legend'] = 'Geodaten';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['description_legend'] = 'Beschreibung';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['publish_legend'] = 'Veröffentlichung';
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['aliasExists'] = 'Alias "%s" ist bereits vergeben.';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_member']['organization_legend'] = 'Organisationen';
|
||||||
|
$GLOBALS['TL_LANG']['tl_member']['organizations'] = ['Organisationen', 'Zugeordnete Organisationen'];
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['eventmanager_legend'] = 'Event-Manager';
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['editPage'] = ['Bearbeitungsseite', 'Bitte wählen Sie die Seite mit dem Bearbeitungsmodul aus.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['listPage'] = ['Listenansicht-Seite', 'Bitte wählen Sie die Seite mit dem Listenmodul aus.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['logoFolder'] = ['Logo-Ordner', 'Bitte wählen Sie den Ordner aus, in dem hochgeladene Organisationslogos gespeichert werden sollen.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['eventFolder'] = ['Event-Ordner', 'Bitte wählen Sie den Ordner aus, in dem eventbezogene Uploads gespeichert werden sollen.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['termsPage'] = ['Seite mit Nutzungsbedingungen', 'Optional: Seite mit den Nutzungsbedingungen, die im Frontend beim Zustimmungs-Label verlinkt wird.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['frontendAuthorId'] = ['Backend Benutzer ID', 'ID des Backend-Benutzers, der als Autor für frontendseitig angelegte Events gesetzt wird.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['frontendArchiveId'] = ['ID des Newsarchivs', 'Archiv-ID (pid), in das frontendseitig angelegte Events gespeichert werden.'];
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['title'] = ['Titel', 'Name der Organisation'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['alias'] = ['Alias', 'Eindeutiger Alias der Organisation'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['logo'] = ['Logo', 'Logo-Datei'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['street'] = ['Straße', 'Straße'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['street2'] = ['Adresszusatz', 'Adresszusatz'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['postal'] = ['PLZ', 'Postleitzahl'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['city'] = ['Ort', 'Ort'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['state'] = ['Bundesland', 'Bundesland'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['country'] = ['Land', 'Land'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['phone'] = ['Telefon', 'Telefonnummer'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['email'] = ['E-Mail', 'E-Mail-Adresse'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['website'] = ['Webseite', 'Webseite'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['lat'] = ['Breitengrad', 'Breitengrad'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['lng'] = ['Längengrad', 'Längengrad'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['description'] = ['Beschreibung', 'Beschreibung'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['published'] = ['Veröffentlicht', 'Organisation veröffentlichen'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['isExternal'] = ['Extern', 'Externe Organisation'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type'] = ['Typ', 'Mehrere Typen auswählbar.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['members'] = ['Mitglieder', 'Zugeordnete Mitglieder'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type_options']['accommodation'] = 'Unterkunft';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type_options']['shopping'] = 'Shopping';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type_options']['culture'] = 'Kultur';
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['title_legend'] = 'Titel';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['address_legend'] = 'Adresse';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['contact_legend'] = 'Kontakt';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['geo_legend'] = 'Geodaten';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['description_legend'] = 'Beschreibung';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['relation_legend'] = 'Beziehungen';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['publish_legend'] = 'Veröffentlichung';
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['memberFallback'] = 'Mitglied #%s';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['aliasExists'] = 'Alias "%s" ist bereits vergeben.';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['MOD']['eventmanager_organisationen'] = ['Organizations', 'Manage organizations'];
|
||||||
|
$GLOBALS['TL_LANG']['MOD']['eventmanager_veranstaltungsorte'] = ['Locations', 'Manage locations'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['eventmanager'] = 'Event manager';
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['member_organizations'] = ['My organizations', 'Lists organizations of the logged-in member.'];
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['organization_edit'] = ['Edit organization', 'Edit form for one organization.'];
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['member_events'] = ['My events', 'Lists events of the member organizations.'];
|
||||||
|
$GLOBALS['TL_LANG']['FMD']['event_edit'] = ['Edit event', 'Edit form for one event.'];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['organization_legend'] = 'Organizations and location';
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['location_id'] = ['Location', 'Assigned location'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type'] = ['Type', 'Multiple types can be selected.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['organizations'] = ['Organizations', 'Assigned organizations'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['photographer'] = ['Author/Photographer', 'Please provide the image author/photographer and make sure you have permission to use the image.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['isSoldOut'] = ['Sold out', 'This event is sold out.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['isCanceled'] = ['Canceled', 'This event has been canceled.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['termsAccepted'] = ['Terms accepted', 'The terms of use have been accepted.'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type_options']['accommodation'] = 'Accommodation';
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type_options']['shopping'] = 'Shopping';
|
||||||
|
$GLOBALS['TL_LANG']['tl_calendar_events']['type_options']['culture'] = 'Culture';
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['title'] = ['Title', 'Location name'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['alias'] = ['Alias', 'Unique location alias'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['description'] = ['Description', 'Description'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['street'] = ['Street', 'Street'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['street2'] = ['Address line 2', 'Address line 2'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['postal'] = ['Postal code', 'Postal code'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['city'] = ['City', 'City'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['state'] = ['State', 'State'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['country'] = ['Country', 'Country'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['lat'] = ['Latitude', 'Latitude'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['lng'] = ['Longitude', 'Longitude'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['image'] = ['Image', 'Image file'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['published'] = ['Published', 'Publish location'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['title_legend'] = 'Title';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['address_legend'] = 'Address';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['geo_legend'] = 'Geo data';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['description_legend'] = 'Description';
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['publish_legend'] = 'Publication';
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_location']['aliasExists'] = 'Alias "%s" already exists.';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_member']['organization_legend'] = 'Organizations';
|
||||||
|
$GLOBALS['TL_LANG']['tl_member']['organizations'] = ['Organizations', 'Assigned organizations'];
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['eventmanager_legend'] = 'Event manager';
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['editPage'] = ['Edit page', 'Please select the page containing the edit module.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['listPage'] = ['List page', 'Please select the page containing the list module.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['logoFolder'] = ['Logo folder', 'Please select the folder where uploaded organization logos should be stored.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['eventFolder'] = ['Event folder', 'Please select the folder where event-related uploads should be stored.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['termsPage'] = ['Terms page', 'Optional: page containing the terms of use linked from the frontend consent label.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['frontendAuthorId'] = ['Backend user ID', 'Backend user ID that should be set as author for events created from the frontend.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_module']['frontendArchiveId'] = ['News archive ID', 'Archive ID (pid) where events created from the frontend should be stored.'];
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['title'] = ['Title', 'Organization name'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['alias'] = ['Alias', 'Unique organization alias'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['logo'] = ['Logo', 'Logo file'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['street'] = ['Street', 'Street'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['street2'] = ['Address line 2', 'Address line 2'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['postal'] = ['Postal code', 'Postal code'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['city'] = ['City', 'City'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['state'] = ['State', 'State'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['country'] = ['Country', 'Country'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['phone'] = ['Phone', 'Phone number'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['email'] = ['Email', 'Email address'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['website'] = ['Website', 'Website'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['lat'] = ['Latitude', 'Latitude'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['lng'] = ['Longitude', 'Longitude'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['description'] = ['Description', 'Description'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['published'] = ['Published', 'Publish organization'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['isExternal'] = ['External', 'External organization'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type'] = ['Type', 'Multiple types can be selected.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['members'] = ['Members', 'Assigned members'];
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type_options']['accommodation'] = 'Accommodation';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type_options']['shopping'] = 'Shopping';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['type_options']['culture'] = 'Culture';
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['title_legend'] = 'Title';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['address_legend'] = 'Address';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['contact_legend'] = 'Contact';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['geo_legend'] = 'Geo data';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['description_legend'] = 'Description';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['relation_legend'] = 'Relations';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['publish_legend'] = 'Publication';
|
||||||
|
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['memberFallback'] = 'Member #%s';
|
||||||
|
$GLOBALS['TL_LANG']['tl_organization']['aliasExists'] = 'Alias "%s" already exists.';
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
{% extends "@Contao/frontend_module/_base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if error is defined and error %}
|
||||||
|
<p role="alert">{{ error }}</p>
|
||||||
|
<p><a href="{{ backUrl }}">Zurück</a></p>
|
||||||
|
{% elseif form is defined and form %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/airbnb.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/de.js"></script>
|
||||||
|
<script src="https://unpkg.com/filepond/dist/filepond.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.js"></script>
|
||||||
|
|
||||||
|
{{ form_start(form, { action: app.request.uri, attr: { 'aria-live': 'polite' } }) }}
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" id="remove_image" name="remove_image" value="0">
|
||||||
|
|
||||||
|
{{ form_row(form.title) }}
|
||||||
|
{{ form_row(form.startDate) }}
|
||||||
|
{{ form_row(form.endDate) }}
|
||||||
|
{{ form_row(form.addTime) }}
|
||||||
|
<div id="event-time-wrap" style="display:none;" hidden aria-hidden="true">
|
||||||
|
{{ form_row(form.startTime) }}
|
||||||
|
{{ form_row(form.endTime) }}
|
||||||
|
</div>
|
||||||
|
{% if form.organization_ids is defined %}
|
||||||
|
{{ form_row(form.organization_ids) }}
|
||||||
|
{% endif %}
|
||||||
|
{{ form_row(form.location_id) }}
|
||||||
|
{{ form_row(form.type) }}
|
||||||
|
{{ form_row(form.teaser) }}
|
||||||
|
{{ form_row(form.description) }}
|
||||||
|
{{ form_row(form.url) }}
|
||||||
|
{{ form_row(form.addImage) }}
|
||||||
|
|
||||||
|
<div id="event-image-upload-wrap" style="display:none;" hidden aria-hidden="true">
|
||||||
|
{{ form_row(form.eventUpload) }}
|
||||||
|
{{ form_row(form.photographer) }}
|
||||||
|
<p class="help-text">Die Angabe des Urhebers ist notwendig. Ihnen muss eine Genehmigung zur Verwendung des Bildes vorliegen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_row(form.termsAccepted) }}
|
||||||
|
{{ form_row(form.isSoldOut) }}
|
||||||
|
{{ form_row(form.isCanceled) }}
|
||||||
|
{{ form_row(form.published) }}
|
||||||
|
<div class="actions" aria-label="Formularaktionen">
|
||||||
|
<button type="submit" id="save-btn" disabled>Speichern</button>
|
||||||
|
<button type="submit" id="save-back-btn" name="save_back" value="1" disabled>Speichern und Zurück</button>
|
||||||
|
<a href="{{ backUrl }}" aria-label="Zurück zur vorherigen Seite">Zurück</a>
|
||||||
|
</div>
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const locationSelect = document.querySelector('select.js-location-choice');
|
||||||
|
const organizationSelect = document.querySelector('select.js-organization-choice');
|
||||||
|
const typeSelect = document.querySelector('select.js-event-type-choice');
|
||||||
|
|
||||||
|
if (organizationSelect && typeof window.Choices === 'function') {
|
||||||
|
new window.Choices(organizationSelect, {
|
||||||
|
searchEnabled: true,
|
||||||
|
shouldSort: false,
|
||||||
|
removeItemButton: true,
|
||||||
|
itemSelectText: '',
|
||||||
|
placeholder: true,
|
||||||
|
placeholderValue: organizationSelect.dataset.placeholder || 'Veranstalter suchen …',
|
||||||
|
noResultsText: 'Keine Treffer',
|
||||||
|
noChoicesText: 'Keine Optionen verfügbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locationSelect && typeof window.Choices === 'function') {
|
||||||
|
new window.Choices(locationSelect, {
|
||||||
|
searchEnabled: true,
|
||||||
|
shouldSort: false,
|
||||||
|
itemSelectText: '',
|
||||||
|
placeholder: true,
|
||||||
|
placeholderValue: locationSelect.dataset.placeholder || 'Suchen …',
|
||||||
|
noResultsText: 'Keine Treffer',
|
||||||
|
noChoicesText: 'Keine Optionen verfügbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeSelect && typeof window.Choices === 'function') {
|
||||||
|
new window.Choices(typeSelect, {
|
||||||
|
searchEnabled: false,
|
||||||
|
shouldSort: false,
|
||||||
|
removeItemButton: true,
|
||||||
|
itemSelectText: '',
|
||||||
|
placeholder: true,
|
||||||
|
placeholderValue: typeSelect.dataset.placeholder || 'Typen suchen …',
|
||||||
|
noResultsText: 'Keine Treffer',
|
||||||
|
noChoicesText: 'Keine Optionen verfügbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.flatpickr === 'function') {
|
||||||
|
const dateInputs = document.querySelectorAll('input.js-flatpickr-date');
|
||||||
|
dateInputs.forEach(function (input) {
|
||||||
|
window.flatpickr(input, {
|
||||||
|
locale: 'de',
|
||||||
|
dateFormat: 'Y-m-d',
|
||||||
|
allowInput: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeInputs = document.querySelectorAll('input.js-flatpickr-time');
|
||||||
|
timeInputs.forEach(function (input) {
|
||||||
|
window.flatpickr(input, {
|
||||||
|
locale: 'de',
|
||||||
|
enableTime: true,
|
||||||
|
noCalendar: true,
|
||||||
|
dateFormat: 'H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
allowInput: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addImageToggle = document.querySelector('input.js-add-image-toggle');
|
||||||
|
const addTimeToggle = document.querySelector('input.js-add-time-toggle');
|
||||||
|
const timeWrap = document.getElementById('event-time-wrap');
|
||||||
|
const startTimeInput = document.getElementById('{{ form.startTime.vars.id }}');
|
||||||
|
const imageWrap = document.getElementById('event-image-upload-wrap');
|
||||||
|
const imageInput = document.querySelector('input.js-event-image-upload');
|
||||||
|
const photographerInput = document.querySelector('input.js-photographer');
|
||||||
|
const removeImageInput = document.getElementById('remove_image');
|
||||||
|
const currentImagePath = {{ (currentImagePath is defined and currentImagePath) ? ('/' ~ currentImagePath)|json_encode|raw : 'null' }};
|
||||||
|
const hadInitialImage = !!currentImagePath;
|
||||||
|
let pondDirty = false;
|
||||||
|
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const saveButton = document.getElementById('save-btn');
|
||||||
|
const saveBackButton = document.getElementById('save-back-btn');
|
||||||
|
if (!form || !saveButton || !saveBackButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const initial = new FormData(form);
|
||||||
|
const hasChanges = () => {
|
||||||
|
const current = new FormData(form);
|
||||||
|
for (const [key, value] of current.entries()) {
|
||||||
|
if (initial.getAll(key).join(',') !== current.getAll(key).join(',')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const updateButtons = () => {
|
||||||
|
const changed = hasChanges() || pondDirty;
|
||||||
|
saveButton.disabled = !changed;
|
||||||
|
saveBackButton.disabled = !changed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setImageVisibility = () => {
|
||||||
|
if (!addImageToggle || !imageWrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addImageToggle.setAttribute('aria-controls', 'event-image-upload-wrap');
|
||||||
|
addImageToggle.setAttribute('aria-expanded', addImageToggle.checked ? 'true' : 'false');
|
||||||
|
|
||||||
|
imageWrap.style.display = addImageToggle.checked ? '' : 'none';
|
||||||
|
imageWrap.hidden = !addImageToggle.checked;
|
||||||
|
imageWrap.setAttribute('aria-hidden', addImageToggle.checked ? 'false' : 'true');
|
||||||
|
|
||||||
|
if (photographerInput) {
|
||||||
|
photographerInput.required = !!addImageToggle.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addImageToggle.checked && removeImageInput) {
|
||||||
|
removeImageInput.value = hadInitialImage ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTimeVisibility = () => {
|
||||||
|
if (!addTimeToggle || !timeWrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimeToggle.setAttribute('aria-controls', 'event-time-wrap');
|
||||||
|
addTimeToggle.setAttribute('aria-expanded', addTimeToggle.checked ? 'true' : 'false');
|
||||||
|
|
||||||
|
timeWrap.style.display = addTimeToggle.checked ? '' : 'none';
|
||||||
|
timeWrap.hidden = !addTimeToggle.checked;
|
||||||
|
timeWrap.setAttribute('aria-hidden', addTimeToggle.checked ? 'false' : 'true');
|
||||||
|
|
||||||
|
if (startTimeInput) {
|
||||||
|
startTimeInput.required = !!addTimeToggle.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (addTimeToggle) {
|
||||||
|
addTimeToggle.addEventListener('change', setTimeVisibility);
|
||||||
|
setTimeVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addImageToggle) {
|
||||||
|
addImageToggle.addEventListener('change', setImageVisibility);
|
||||||
|
setImageVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageInput && typeof window.FilePond !== 'undefined') {
|
||||||
|
if (typeof window.FilePondPluginImagePreview !== 'undefined') {
|
||||||
|
window.FilePond.registerPlugin(window.FilePondPluginImagePreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePondOptions = {
|
||||||
|
instantUpload: false,
|
||||||
|
storeAsFile: true,
|
||||||
|
allowMultiple: false,
|
||||||
|
allowReplace: true,
|
||||||
|
credits: false,
|
||||||
|
acceptedFileTypes: ['image/*'],
|
||||||
|
labelIdle: 'Bild hierher ziehen oder <span class="filepond--label-action">durchsuchen</span>'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentImagePath) {
|
||||||
|
filePondOptions.files = [
|
||||||
|
{
|
||||||
|
source: currentImagePath,
|
||||||
|
options: {
|
||||||
|
type: 'local'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
filePondOptions.server = {
|
||||||
|
load: (source, load, error, progress, abort) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', source);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
load(xhr.response);
|
||||||
|
} else {
|
||||||
|
error('Laden fehlgeschlagen');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
error('Laden fehlgeschlagen');
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
xhr.abort();
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pond = window.FilePond.create(imageInput, filePondOptions);
|
||||||
|
|
||||||
|
const syncPondState = () => {
|
||||||
|
const files = pond.getFiles();
|
||||||
|
const hasLocalFile = files.some((file) => window.FilePond.FileOrigin && file.origin === window.FilePond.FileOrigin.LOCAL);
|
||||||
|
const hasNewFile = files.some((file) => window.FilePond.FileOrigin && file.origin !== window.FilePond.FileOrigin.LOCAL);
|
||||||
|
const imageEnabled = addImageToggle ? addImageToggle.checked : true;
|
||||||
|
|
||||||
|
pondDirty = imageEnabled && (hasNewFile || (hadInitialImage && !hasLocalFile));
|
||||||
|
|
||||||
|
if (removeImageInput) {
|
||||||
|
if (!imageEnabled) {
|
||||||
|
removeImageInput.value = hadInitialImage ? '1' : '0';
|
||||||
|
} else {
|
||||||
|
removeImageInput.value = hadInitialImage && !hasLocalFile && !hasNewFile ? '1' : '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
syncPondState();
|
||||||
|
|
||||||
|
pond.on('removefile', function () {
|
||||||
|
syncPondState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pond.on('addfile', function () {
|
||||||
|
syncPondState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pond.on('updatefiles', function () {
|
||||||
|
syncPondState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('input', function () {
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
form.addEventListener('change', function () {
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<p>Kein Formular verfügbar.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{% extends "@Contao/frontend_module/_base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Kommende Veranstaltungen</h2>
|
||||||
|
{% if upcomingEvents is empty %}
|
||||||
|
<p>Keine kommenden Veranstaltungen gefunden.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul class="member-events member-events-upcoming" aria-label="Liste kommender Veranstaltungen">
|
||||||
|
{% for item in upcomingEvents %}
|
||||||
|
<li>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<span>
|
||||||
|
{% if item.startDate %}
|
||||||
|
<time datetime="{{ item.startDate|date('Y-m-d') }}">{{ item.startDate|date('d.m.Y') }}</time>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if isEditor %}
|
||||||
|
<form method="post" style="display:inline;" aria-label="Sichtbarkeit für {{ item.title }} ändern">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="toggle_published">
|
||||||
|
<input type="hidden" name="event_id" value="{{ item.id }}">
|
||||||
|
<button type="submit" aria-label="{{ item.published ? 'Auf inaktiv setzen' : 'Auf aktiv setzen' }} für {{ item.title }}">{{ item.published ? 'aktiv' : 'inaktiv' }}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if item.editUrl %}
|
||||||
|
<a href="{{ item.editUrl }}" aria-label="{{ item.title }} bearbeiten">Bearbeiten</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" style="display:inline;" aria-label="{{ item.title }} duplizieren">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="duplicate">
|
||||||
|
<input type="hidden" name="event_id" value="{{ item.id }}">
|
||||||
|
<button type="submit" aria-label="{{ item.title }} duplizieren">Duplizieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" style="display:inline;" onsubmit="return confirm('wirklich löschen?');" aria-label="{{ item.title }} löschen">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="event_id" value="{{ item.id }}">
|
||||||
|
<button type="submit" aria-label="{{ item.title }} löschen">Löschen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if canCreateEvent is defined and canCreateEvent %}
|
||||||
|
<form method="post" aria-label="Neue Veranstaltung erstellen">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="create">
|
||||||
|
<button type="submit" aria-label="Neue Veranstaltung erstellen">Neue Veranstaltung erstellen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>Vergangene Veranstaltungen</h2>
|
||||||
|
{% if pastEvents is empty %}
|
||||||
|
<p>Keine vergangenen Veranstaltungen gefunden.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul class="member-events member-events-past" aria-label="Liste vergangener Veranstaltungen">
|
||||||
|
{% for item in pastEvents %}
|
||||||
|
<li>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<span>
|
||||||
|
{% if item.startDate %}
|
||||||
|
<time datetime="{{ item.startDate|date('Y-m-d') }}">{{ item.startDate|date('d.m.Y') }}</time>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if isEditor %}
|
||||||
|
<form method="post" style="display:inline;" aria-label="Sichtbarkeit für {{ item.title }} ändern">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="toggle_published">
|
||||||
|
<input type="hidden" name="event_id" value="{{ item.id }}">
|
||||||
|
<button type="submit" aria-label="{{ item.published ? 'Auf inaktiv setzen' : 'Auf aktiv setzen' }} für {{ item.title }}">{{ item.published ? 'aktiv' : 'inaktiv' }}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if item.editUrl %}
|
||||||
|
<a href="{{ item.editUrl }}" aria-label="{{ item.title }} bearbeiten">Bearbeiten</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" style="display:inline;" aria-label="{{ item.title }} duplizieren">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="duplicate">
|
||||||
|
<input type="hidden" name="event_id" value="{{ item.id }}">
|
||||||
|
<button type="submit" aria-label="{{ item.title }} duplizieren">Duplizieren</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" style="display:inline;" onsubmit="return confirm('wirklich löschen?');" aria-label="{{ item.title }} löschen">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="event_id" value="{{ item.id }}">
|
||||||
|
<button type="submit" aria-label="{{ item.title }} löschen">Löschen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "@Contao/frontend_module/_base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Organisationen</h2>
|
||||||
|
{% if organizations is empty %}
|
||||||
|
<p>Keine Organisationen gefunden.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul class="member-organizations" aria-label="Liste der Organisationen">
|
||||||
|
{% for item in organizations %}
|
||||||
|
<li>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
{% if item.editUrl %}
|
||||||
|
<a href="{{ item.editUrl }}" aria-label="{{ item.title }} bearbeiten">Bearbeiten</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
{% extends "@Contao/frontend_module/_base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if error is defined and error %}
|
||||||
|
<p role="alert">{{ error }}</p>
|
||||||
|
<p><a href="{{ backUrl }}">Zurück</a></p>
|
||||||
|
{% elseif form is defined and form %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/filepond/dist/filepond.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.js"></script>
|
||||||
|
|
||||||
|
{{ form_start(form, { attr: { 'aria-live': 'polite' } }) }}
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
|
||||||
|
<input type="hidden" id="remove_logo" name="remove_logo" value="0">
|
||||||
|
{{ form_widget(form) }}
|
||||||
|
<div class="actions" aria-label="Formularaktionen">
|
||||||
|
<button type="submit" id="save-btn" disabled>Speichern</button>
|
||||||
|
<button type="submit" id="save-back-btn" name="save_back" value="1" disabled>Speichern und Zurück</button>
|
||||||
|
<a href="{{ backUrl }}" aria-label="Zurück zur vorherigen Seite">Zurück</a>
|
||||||
|
</div>
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const typeSelect = document.querySelector('select.js-organization-type-choice');
|
||||||
|
|
||||||
|
if (typeSelect && typeof window.Choices === 'function') {
|
||||||
|
new window.Choices(typeSelect, {
|
||||||
|
searchEnabled: false,
|
||||||
|
shouldSort: false,
|
||||||
|
removeItemButton: true,
|
||||||
|
itemSelectText: '',
|
||||||
|
placeholder: true,
|
||||||
|
placeholderValue: typeSelect.dataset.placeholder || 'Typ auswählen …',
|
||||||
|
noResultsText: 'Keine Treffer',
|
||||||
|
noChoicesText: 'Keine Optionen verfügbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLogoPath = {{ (currentLogoPath is defined and currentLogoPath) ? ('/' ~ currentLogoPath)|json_encode|raw : 'null' }};
|
||||||
|
const logoInput = document.querySelector('input.js-logo-upload');
|
||||||
|
const removeLogoInput = document.getElementById('remove_logo');
|
||||||
|
const hadInitialLogo = !!currentLogoPath;
|
||||||
|
let pondDirty = false;
|
||||||
|
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const saveButton = document.getElementById('save-btn');
|
||||||
|
const saveBackButton = document.getElementById('save-back-btn');
|
||||||
|
if (!form || !saveButton || !saveBackButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initial = new FormData(form);
|
||||||
|
const hasChanges = () => {
|
||||||
|
const current = new FormData(form);
|
||||||
|
for (const [key, value] of current.entries()) {
|
||||||
|
if (initial.getAll(key).join(',') !== current.getAll(key).join(',')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const updateButtons = () => {
|
||||||
|
const changed = hasChanges() || pondDirty;
|
||||||
|
saveButton.disabled = !changed;
|
||||||
|
saveBackButton.disabled = !changed;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (logoInput && typeof window.FilePond !== 'undefined') {
|
||||||
|
if (typeof window.FilePondPluginImagePreview !== 'undefined') {
|
||||||
|
window.FilePond.registerPlugin(window.FilePondPluginImagePreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePondOptions = {
|
||||||
|
instantUpload: false,
|
||||||
|
storeAsFile: true,
|
||||||
|
allowMultiple: false,
|
||||||
|
allowReplace: true,
|
||||||
|
credits: false,
|
||||||
|
acceptedFileTypes: ['image/*'],
|
||||||
|
labelIdle: 'Logo hierher ziehen oder <span class="filepond--label-action">durchsuchen</span>'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentLogoPath) {
|
||||||
|
filePondOptions.files = [
|
||||||
|
{
|
||||||
|
source: currentLogoPath,
|
||||||
|
options: {
|
||||||
|
type: 'local'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
filePondOptions.server = {
|
||||||
|
load: (source, load, error, progress, abort) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', source);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
load(xhr.response);
|
||||||
|
} else {
|
||||||
|
error('Laden fehlgeschlagen');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
error('Laden fehlgeschlagen');
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
xhr.abort();
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pond = window.FilePond.create(logoInput, filePondOptions);
|
||||||
|
|
||||||
|
const syncPondState = () => {
|
||||||
|
const files = pond.getFiles();
|
||||||
|
const hasLocalFile = files.some((file) => window.FilePond.FileOrigin && file.origin === window.FilePond.FileOrigin.LOCAL);
|
||||||
|
const hasNewFile = files.some((file) => window.FilePond.FileOrigin && file.origin !== window.FilePond.FileOrigin.LOCAL);
|
||||||
|
|
||||||
|
pondDirty = hasNewFile || (hadInitialLogo && !hasLocalFile);
|
||||||
|
|
||||||
|
if (removeLogoInput) {
|
||||||
|
removeLogoInput.value = hadInitialLogo && !hasLocalFile && !hasNewFile ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
syncPondState();
|
||||||
|
|
||||||
|
pond.on('removefile', function (error, file) {
|
||||||
|
if (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncPondState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pond.on('addfile', function () {
|
||||||
|
syncPondState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pond.on('updatefiles', function () {
|
||||||
|
syncPondState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
form.addEventListener('input', function () {
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
form.addEventListener('change', function () {
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<p>Kein Formular verfügbar.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Contao\Manager;
|
||||||
|
|
||||||
|
use Contao\CalendarBundle\ContaoCalendarBundle;
|
||||||
|
use Contao\CoreBundle\ContaoCoreBundle;
|
||||||
|
use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
|
||||||
|
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
|
||||||
|
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
|
||||||
|
use MummertMedia\EventManagerBundle\MummertMediaEventManagerBundle;
|
||||||
|
|
||||||
|
class Plugin implements BundlePluginInterface
|
||||||
|
{
|
||||||
|
public function getBundles(ParserInterface $parser): iterable
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
BundleConfig::create(MummertMediaEventManagerBundle::class)
|
||||||
|
->setLoadAfter([ContaoCoreBundle::class, ContaoCalendarBundle::class]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Controller\Frontend;
|
||||||
|
|
||||||
|
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
|
||||||
|
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
|
||||||
|
use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||||
|
use Contao\Dbafs;
|
||||||
|
use Contao\FilesModel;
|
||||||
|
use Contao\FrontendUser;
|
||||||
|
use Contao\ModuleModel;
|
||||||
|
use Contao\PageModel;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
use MummertMedia\EventManagerBundle\Form\EventType;
|
||||||
|
use MummertMedia\EventManagerBundle\Service\EventRepository;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
#[AsFrontendModule(type: 'event_edit', category: 'eventmanager', template: 'frontend/event_edit')]
|
||||||
|
class EventEditController extends AbstractFrontendModuleController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EventRepository $eventRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
$backUrl = $this->resolveBackUrl($request, $model);
|
||||||
|
$eventParam = $request->query->get('event');
|
||||||
|
$isCreateMode = '1' === (string) $request->query->get('create', '0') && (null === $eventParam || (int) $eventParam <= 0);
|
||||||
|
|
||||||
|
if (!$user instanceof FrontendUser) {
|
||||||
|
$template->set('error', 'Bitte zuerst als Mitglied einloggen.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventId = $isCreateMode ? 0 : (int) $eventParam;
|
||||||
|
$event = null;
|
||||||
|
|
||||||
|
if (!$isCreateMode) {
|
||||||
|
if ($eventId <= 0 || !$this->eventRepository->memberHasEvent((int) $user->id, $eventId)) {
|
||||||
|
$template->set('error', 'Keine Berechtigung für diese Veranstaltung oder ungültige ID.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$event = $this->eventRepository->findById($eventId);
|
||||||
|
|
||||||
|
if (null === $event) {
|
||||||
|
$template->set('error', 'Veranstaltung nicht gefunden.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isCreateMode) {
|
||||||
|
$event = [
|
||||||
|
'title' => '',
|
||||||
|
'startDate' => null,
|
||||||
|
'endDate' => null,
|
||||||
|
'addTime' => '',
|
||||||
|
'startTime' => null,
|
||||||
|
'endTime' => null,
|
||||||
|
'location_id' => 0,
|
||||||
|
'type' => null,
|
||||||
|
'teaser' => '',
|
||||||
|
'description' => '',
|
||||||
|
'url' => '',
|
||||||
|
'photographer' => '',
|
||||||
|
'addImage' => '',
|
||||||
|
'termsAccepted' => '',
|
||||||
|
'isSoldOut' => '',
|
||||||
|
'isCanceled' => '',
|
||||||
|
'published' => '',
|
||||||
|
'singleSRC' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$memberOrganizationIds = $this->eventRepository->getOrganizationIdsForMember((int) $user->id);
|
||||||
|
$organizationChoices = $this->eventRepository->getOrganizationChoicesForMember((int) $user->id);
|
||||||
|
$showOrganization = count($memberOrganizationIds) > 1;
|
||||||
|
$currentOrganizationIds = $this->eventRepository->getOrganizationIdsForEvent($eventId);
|
||||||
|
|
||||||
|
$formData = [
|
||||||
|
'title' => (string) ($event['title'] ?? ''),
|
||||||
|
'startDate' => !empty($event['startDate']) ? date('Y-m-d', (int) $event['startDate']) : '',
|
||||||
|
'endDate' => !empty($event['endDate']) ? date('Y-m-d', (int) $event['endDate']) : '',
|
||||||
|
'addTime' => '1' === (string) ($event['addTime'] ?? ''),
|
||||||
|
'startTime' => ('1' === (string) ($event['addTime'] ?? '') && !empty($event['startTime'])) ? date('H:i', (int) $event['startTime']) : '',
|
||||||
|
'endTime' => $this->resolveFormEndTime($event),
|
||||||
|
'location_id' => (int) ($event['location_id'] ?? 0),
|
||||||
|
'type' => StringUtil::deserialize($event['type'] ?? null, true),
|
||||||
|
'teaser' => (string) ($event['teaser'] ?? ''),
|
||||||
|
'description' => (string) ($event['description'] ?? ''),
|
||||||
|
'url' => (string) ($event['url'] ?? ''),
|
||||||
|
'photographer' => (string) ($event['photographer'] ?? ''),
|
||||||
|
'addImage' => '1' === (string) ($event['addImage'] ?? ''),
|
||||||
|
'termsAccepted' => '1' === (string) ($event['termsAccepted'] ?? ''),
|
||||||
|
'isSoldOut' => '1' === (string) ($event['isSoldOut'] ?? ''),
|
||||||
|
'isCanceled' => '1' === (string) ($event['isCanceled'] ?? ''),
|
||||||
|
'published' => '1' === (string) ($event['published'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($showOrganization) {
|
||||||
|
$allowedOrganizationIds = array_values(array_unique(array_map('intval', $memberOrganizationIds)));
|
||||||
|
$selectedOrganizationIds = array_values(array_intersect($currentOrganizationIds, $allowedOrganizationIds));
|
||||||
|
|
||||||
|
if ([] === $selectedOrganizationIds && [] !== $allowedOrganizationIds) {
|
||||||
|
$selectedOrganizationIds = [(int) $allowedOrganizationIds[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$formData['organization_ids'] = $selectedOrganizationIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentImagePath = null;
|
||||||
|
|
||||||
|
if (!empty($event['singleSRC'])) {
|
||||||
|
$imageModel = FilesModel::findByUuid((string) $event['singleSRC']);
|
||||||
|
|
||||||
|
if (null !== $imageModel) {
|
||||||
|
$currentImagePath = $imageModel->path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(EventType::class, $formData, [
|
||||||
|
'location_choices' => $this->eventRepository->getLocationChoices(),
|
||||||
|
'organization_choices' => $organizationChoices,
|
||||||
|
'selected_organization_ids' => $showOrganization ? ($formData['organization_ids'] ?? []) : [],
|
||||||
|
'show_organization' => $showOrganization,
|
||||||
|
'terms_page_url' => $this->resolveTermsPageUrl($model),
|
||||||
|
]);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted()) {
|
||||||
|
$submittedData = (array) $form->getData();
|
||||||
|
|
||||||
|
if (!empty($submittedData['addImage']) && '' === trim((string) ($submittedData['photographer'] ?? ''))) {
|
||||||
|
$form->get('photographer')->addError(new FormError('Die Angabe des Urhebers ist notwendig. Ihnen muss eine Genehmigung zur Verwendung des Bildes vorliegen.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($submittedData['addTime']) && '' === trim((string) ($submittedData['startTime'] ?? ''))) {
|
||||||
|
$form->get('startTime')->addError(new FormError('Bitte geben Sie eine Startzeit an.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$submittedData = (array) $form->getData();
|
||||||
|
|
||||||
|
$selectedOrganizationIds = [];
|
||||||
|
|
||||||
|
if ($showOrganization) {
|
||||||
|
$selectedOrganizationIds = array_map('intval', (array) $form->get('organization_ids')->getData());
|
||||||
|
} elseif ([] !== $memberOrganizationIds) {
|
||||||
|
$selectedOrganizationIds = [(int) $memberOrganizationIds[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedOrganizationIds = array_values(array_unique(array_map('intval', $memberOrganizationIds)));
|
||||||
|
$selectedOrganizationIds = array_values(array_intersect($selectedOrganizationIds, $allowedOrganizationIds));
|
||||||
|
|
||||||
|
if ([] === $selectedOrganizationIds) {
|
||||||
|
if ($showOrganization) {
|
||||||
|
$form->get('organization_ids')->addError(new FormError('Bitte mindestens eine Organisation auswählen.'));
|
||||||
|
} else {
|
||||||
|
$form->addError(new FormError('Keine gültige Veranstalter-Zuordnung vorhanden.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && !$form->isValid()) {
|
||||||
|
$template->set('form', $form->createView());
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
$template->set('requestToken', $this->container->get('contao.csrf.token_manager')->getDefaultTokenValue());
|
||||||
|
$template->set('currentImagePath', $currentImagePath);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isCreateMode) {
|
||||||
|
$createdEventId = $this->eventRepository->createForMember(
|
||||||
|
(int) $user->id,
|
||||||
|
(int) ($model->frontendAuthorId ?? 0),
|
||||||
|
(int) ($model->frontendArchiveId ?? 0),
|
||||||
|
$submittedData,
|
||||||
|
$selectedOrganizationIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (null === $createdEventId) {
|
||||||
|
$form->addError(new FormError('Die Veranstaltung konnte nicht erstellt werden.'));
|
||||||
|
$template->set('form', $form->createView());
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
$template->set('requestToken', $this->container->get('contao.csrf.token_manager')->getDefaultTokenValue());
|
||||||
|
$template->set('currentImagePath', $currentImagePath);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventId = $createdEventId;
|
||||||
|
} else {
|
||||||
|
if ($showOrganization) {
|
||||||
|
$this->eventRepository->assignEventToOrganizations($eventId, $selectedOrganizationIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->eventRepository->update($eventId, $submittedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$useImage = !empty($submittedData['addImage']);
|
||||||
|
$removeImage = '1' === (string) $request->request->get('remove_image', '0');
|
||||||
|
$uploadedImage = $form->get('eventUpload')->getData();
|
||||||
|
|
||||||
|
if (!$useImage) {
|
||||||
|
$this->eventRepository->updateImageFields($eventId, false, null);
|
||||||
|
} elseif ($uploadedImage instanceof UploadedFile) {
|
||||||
|
$newImageUuid = $this->storeEventImage($uploadedImage, $model, $eventId);
|
||||||
|
|
||||||
|
if (null !== $newImageUuid) {
|
||||||
|
$this->eventRepository->updateImageFields($eventId, true, $newImageUuid);
|
||||||
|
}
|
||||||
|
} elseif ($removeImage) {
|
||||||
|
$this->eventRepository->updateImageFields($eventId, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->request->has('save_back')) {
|
||||||
|
return new RedirectResponse($backUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isCreateMode) {
|
||||||
|
return new RedirectResponse($this->buildEventEditUrl($eventId, $backUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedirectResponse($request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
$template->set('form', $form->createView());
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
$template->set('requestToken', $this->container->get('contao.csrf.token_manager')->getDefaultTokenValue());
|
||||||
|
$template->set('currentImagePath', $currentImagePath);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeEventImage(UploadedFile $uploadedFile, ModuleModel $model, int $eventId): ?string
|
||||||
|
{
|
||||||
|
if (empty($model->eventFolder)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$folderModel = FilesModel::findByUuid((string) $model->eventFolder);
|
||||||
|
|
||||||
|
if (null === $folderModel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$organization = $this->eventRepository->findPrimaryOrganizationForEvent($eventId);
|
||||||
|
|
||||||
|
if (null === $organization) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$titleSlug = strtolower(StringUtil::generateAlias((string) $organization['title']));
|
||||||
|
$titleSlug = preg_replace('/[^a-z0-9-]+/', '-', $titleSlug ?? '') ?? '';
|
||||||
|
$titleSlug = trim($titleSlug, '-');
|
||||||
|
$titleSlug = substr($titleSlug, 0, 12);
|
||||||
|
|
||||||
|
if ('' === $titleSlug) {
|
||||||
|
$titleSlug = 'organization';
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadBasePath = trim((string) $folderModel->path, '/');
|
||||||
|
|
||||||
|
if ('' === $uploadBasePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetRelativeDir = sprintf('%s/org-%d-%s', $uploadBasePath, (int) $organization['id'], $titleSlug);
|
||||||
|
$projectDir = (string) $this->getParameter('kernel.project_dir');
|
||||||
|
$targetAbsoluteDir = rtrim($projectDir, '/').'/'.$targetRelativeDir;
|
||||||
|
|
||||||
|
if (!is_dir($targetAbsoluteDir) && !mkdir($targetAbsoluteDir, 0775, true) && !is_dir($targetAbsoluteDir)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||||
|
$safeName = preg_replace('/[^a-zA-Z0-9_-]+/', '-', (string) $originalName);
|
||||||
|
$safeName = trim((string) $safeName, '-');
|
||||||
|
|
||||||
|
if ('' === $safeName) {
|
||||||
|
$safeName = 'event-image';
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = $uploadedFile->guessExtension() ?: $uploadedFile->getClientOriginalExtension() ?: 'bin';
|
||||||
|
$filename = sprintf('%s-%s.%s', $safeName, uniqid('', true), strtolower((string) $extension));
|
||||||
|
|
||||||
|
$uploadedFile->move($targetAbsoluteDir, $filename);
|
||||||
|
|
||||||
|
$relativePath = $targetRelativeDir.'/'.$filename;
|
||||||
|
$fileModel = Dbafs::addResource($relativePath);
|
||||||
|
|
||||||
|
return null !== $fileModel ? (string) $fileModel->uuid : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveBackUrl(Request $request, ModuleModel $model): string
|
||||||
|
{
|
||||||
|
if ((int) ($model->listPage ?? 0) > 0) {
|
||||||
|
$listPage = $this->getContaoAdapter(PageModel::class)->findById((int) $model->listPage);
|
||||||
|
|
||||||
|
if ($listPage instanceof PageModel) {
|
||||||
|
return $this->generateContentUrl($listPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = (string) $request->query->get('ref', '');
|
||||||
|
|
||||||
|
if ('' !== $ref) {
|
||||||
|
$decoded = base64_decode($ref, true);
|
||||||
|
|
||||||
|
if (false !== $decoded && str_starts_with($decoded, '/')) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = $this->getPageModel();
|
||||||
|
|
||||||
|
return $page instanceof PageModel ? $this->generateContentUrl($page) : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTermsPageUrl(ModuleModel $model): ?string
|
||||||
|
{
|
||||||
|
if ((int) ($model->termsPage ?? 0) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$termsPage = $this->getContaoAdapter(PageModel::class)->findById((int) $model->termsPage);
|
||||||
|
|
||||||
|
if (!$termsPage instanceof PageModel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->generateContentUrl($termsPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventEditUrl(int $eventId, string $backUrl): string
|
||||||
|
{
|
||||||
|
$page = $this->getPageModel();
|
||||||
|
|
||||||
|
if (!$page instanceof PageModel) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->generateContentUrl($page, [
|
||||||
|
'event' => (string) $eventId,
|
||||||
|
'ref' => base64_encode($backUrl),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveFormEndTime(array $event): string
|
||||||
|
{
|
||||||
|
if ('1' !== (string) ($event['addTime'] ?? '')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTime = (int) ($event['startTime'] ?? 0);
|
||||||
|
$endTime = (int) ($event['endTime'] ?? 0);
|
||||||
|
|
||||||
|
if ($endTime <= 0 || $startTime <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endTime === $startTime || $endTime - $startTime === 86399) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('H:i', $endTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Controller\Frontend;
|
||||||
|
|
||||||
|
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
|
||||||
|
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
|
||||||
|
use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||||
|
use Contao\FrontendUser;
|
||||||
|
use Contao\ModuleModel;
|
||||||
|
use Contao\PageModel;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
use MummertMedia\EventManagerBundle\Service\EventRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
#[AsFrontendModule(type: 'member_events', category: 'eventmanager', template: 'frontend/member_events')]
|
||||||
|
class MemberEventsController extends AbstractFrontendModuleController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EventRepository $eventRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof FrontendUser) {
|
||||||
|
$template->set('upcomingEvents', []);
|
||||||
|
$template->set('pastEvents', []);
|
||||||
|
$template->set('isEditor', false);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$isEditor = $this->isEditor($user);
|
||||||
|
$editPage = null;
|
||||||
|
|
||||||
|
if ((int) ($model->editPage ?? 0) > 0) {
|
||||||
|
$editPage = $this->getContaoAdapter(PageModel::class)->findById((int) $model->editPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPage = $this->getPageModel();
|
||||||
|
$backUrl = $currentPage instanceof PageModel ? $this->generateContentUrl($currentPage) : '/';
|
||||||
|
|
||||||
|
if ($isEditor && 'POST' === $request->getMethod() && $request->request->has('action')) {
|
||||||
|
$action = (string) $request->request->get('action', '');
|
||||||
|
$eventId = (int) $request->request->get('event_id', 0);
|
||||||
|
|
||||||
|
if (!in_array($action, ['create', 'toggle_published', 'duplicate', 'delete'], true)) {
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('create' === $action && $editPage instanceof PageModel) {
|
||||||
|
return new RedirectResponse($this->generateContentUrl($editPage, [
|
||||||
|
'create' => '1',
|
||||||
|
'ref' => base64_encode($backUrl),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eventId > 0 && $this->eventRepository->memberHasEvent((int) $user->id, $eventId)) {
|
||||||
|
if ('toggle_published' === $action) {
|
||||||
|
$this->eventRepository->togglePublished($eventId);
|
||||||
|
} elseif ('duplicate' === $action) {
|
||||||
|
$this->eventRepository->duplicate($eventId);
|
||||||
|
} elseif ('delete' === $action) {
|
||||||
|
$this->eventRepository->delete($eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedirectResponse($request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = $this->eventRepository->findByMemberId((int) $user->id);
|
||||||
|
|
||||||
|
$upcomingItems = [];
|
||||||
|
$pastItems = [];
|
||||||
|
$today = strtotime('today');
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$editUrl = null;
|
||||||
|
|
||||||
|
if ($isEditor && $editPage instanceof PageModel) {
|
||||||
|
$editUrl = $this->generateContentUrl($editPage, [
|
||||||
|
'event' => (string) $event['id'],
|
||||||
|
'ref' => base64_encode($backUrl),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = [
|
||||||
|
'id' => (int) $event['id'],
|
||||||
|
'title' => (string) ($event['title'] ?? ''),
|
||||||
|
'startDate' => (int) ($event['startDate'] ?? 0),
|
||||||
|
'published' => '1' === (string) ($event['published'] ?? ''),
|
||||||
|
'editUrl' => $editUrl,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ((int) $item['startDate'] >= $today) {
|
||||||
|
$upcomingItems[] = $item;
|
||||||
|
} else {
|
||||||
|
$pastItems[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usort(
|
||||||
|
$upcomingItems,
|
||||||
|
static fn (array $a, array $b): int => ((int) $b['startDate']) <=> ((int) $a['startDate']),
|
||||||
|
);
|
||||||
|
|
||||||
|
usort(
|
||||||
|
$pastItems,
|
||||||
|
static fn (array $a, array $b): int => ((int) $b['startDate']) <=> ((int) $a['startDate']),
|
||||||
|
);
|
||||||
|
|
||||||
|
$template->set('upcomingEvents', $upcomingItems);
|
||||||
|
$template->set('pastEvents', $pastItems);
|
||||||
|
$template->set('isEditor', $isEditor);
|
||||||
|
$template->set('canCreateEvent', $isEditor && $editPage instanceof PageModel);
|
||||||
|
$template->set('requestToken', $this->container->get('contao.csrf.token_manager')->getDefaultTokenValue());
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isEditor(FrontendUser $user): bool
|
||||||
|
{
|
||||||
|
$groups = is_array($user->groups) ? $user->groups : StringUtil::deserialize($user->groups, true);
|
||||||
|
|
||||||
|
return in_array(1, array_map('intval', $groups), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Controller\Frontend;
|
||||||
|
|
||||||
|
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
|
||||||
|
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
|
||||||
|
use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||||
|
use Contao\FrontendUser;
|
||||||
|
use Contao\ModuleModel;
|
||||||
|
use Contao\PageModel;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
use MummertMedia\EventManagerBundle\Service\OrganizationRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
#[AsFrontendModule(type: 'member_organizations', category: 'eventmanager', template: 'frontend/member_organizations')]
|
||||||
|
class MemberOrganizationsController extends AbstractFrontendModuleController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly OrganizationRepository $organizationRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof FrontendUser) {
|
||||||
|
$template->set('organizations', []);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$organizations = $this->organizationRepository->findByMemberId((int) $user->id);
|
||||||
|
$isEditor = $this->isEditor($user);
|
||||||
|
$editPage = null;
|
||||||
|
|
||||||
|
if ((int) ($model->editPage ?? 0) > 0) {
|
||||||
|
$editPage = $this->getContaoAdapter(PageModel::class)->findById((int) $model->editPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPage = $this->getPageModel();
|
||||||
|
$backUrl = $currentPage instanceof PageModel ? $this->generateContentUrl($currentPage) : '/';
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach ($organizations as $organization) {
|
||||||
|
$editUrl = null;
|
||||||
|
|
||||||
|
if ($isEditor && $editPage instanceof PageModel) {
|
||||||
|
$editUrl = $this->generateContentUrl($editPage, [
|
||||||
|
'organization' => (string) $organization['id'],
|
||||||
|
'ref' => base64_encode($backUrl),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items[] = [
|
||||||
|
'id' => (int) $organization['id'],
|
||||||
|
'title' => (string) ($organization['title'] ?? ''),
|
||||||
|
'editUrl' => $editUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$template->set('organizations', $items);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isEditor(FrontendUser $user): bool
|
||||||
|
{
|
||||||
|
$groups = is_array($user->groups) ? $user->groups : StringUtil::deserialize($user->groups, true);
|
||||||
|
|
||||||
|
return in_array(1, array_map('intval', $groups), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Controller\Frontend;
|
||||||
|
|
||||||
|
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
|
||||||
|
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
|
||||||
|
use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||||
|
use Contao\Dbafs;
|
||||||
|
use Contao\FilesModel;
|
||||||
|
use Contao\FrontendUser;
|
||||||
|
use Contao\ModuleModel;
|
||||||
|
use Contao\PageModel;
|
||||||
|
use Contao\StringUtil;
|
||||||
|
use MummertMedia\EventManagerBundle\Form\OrganizationType;
|
||||||
|
use MummertMedia\EventManagerBundle\Service\OrganizationRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
#[AsFrontendModule(type: 'organization_edit', category: 'eventmanager', template: 'frontend/organization_edit')]
|
||||||
|
class OrganizationEditController extends AbstractFrontendModuleController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly OrganizationRepository $organizationRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
$backUrl = $this->resolveBackUrl($request, $model);
|
||||||
|
$organizationParam = $request->query->get('organization');
|
||||||
|
|
||||||
|
if (null === $organizationParam) {
|
||||||
|
$template->set('error', 'Ungültiger Aufruf: Parameter "organization" fehlt.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user instanceof FrontendUser) {
|
||||||
|
$template->set('error', 'Bitte zuerst als Mitglied einloggen.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$organizationId = (int) $organizationParam;
|
||||||
|
|
||||||
|
if ($organizationId <= 0 || !$this->organizationRepository->memberHasOrganization((int) $user->id, $organizationId)) {
|
||||||
|
$template->set('error', 'Keine Berechtigung für diese Organisation oder ungültige ID.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$organization = $this->organizationRepository->findById($organizationId);
|
||||||
|
|
||||||
|
if (null === $organization) {
|
||||||
|
$template->set('error', 'Organisation nicht gefunden.');
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$formData = [
|
||||||
|
'title' => (string) ($organization['title'] ?? ''),
|
||||||
|
'street' => (string) ($organization['street'] ?? ''),
|
||||||
|
'postal' => (string) ($organization['postal'] ?? ''),
|
||||||
|
'city' => (string) ($organization['city'] ?? ''),
|
||||||
|
'state' => (string) ($organization['state'] ?? ''),
|
||||||
|
'country' => (string) ($organization['country'] ?? ''),
|
||||||
|
'phone' => (string) ($organization['phone'] ?? ''),
|
||||||
|
'email' => (string) ($organization['email'] ?? ''),
|
||||||
|
'website' => (string) ($organization['website'] ?? ''),
|
||||||
|
'description' => (string) ($organization['description'] ?? ''),
|
||||||
|
'type' => StringUtil::deserialize($organization['type'] ?? null, true),
|
||||||
|
];
|
||||||
|
|
||||||
|
$currentLogoPath = null;
|
||||||
|
|
||||||
|
if (!empty($organization['logo'])) {
|
||||||
|
$logoModel = FilesModel::findByUuid((string) $organization['logo']);
|
||||||
|
|
||||||
|
if (null !== $logoModel) {
|
||||||
|
$currentLogoPath = $logoModel->path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(OrganizationType::class, $formData);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$submittedData = (array) $form->getData();
|
||||||
|
|
||||||
|
$this->organizationRepository->update($organizationId, $submittedData);
|
||||||
|
|
||||||
|
$deleteLogo = '1' === (string) $request->request->get('remove_logo', '0');
|
||||||
|
$uploadedLogo = $form->get('logoUpload')->getData();
|
||||||
|
|
||||||
|
if ($uploadedLogo instanceof UploadedFile) {
|
||||||
|
$newLogoUuid = $this->storeOrganizationLogo(
|
||||||
|
$uploadedLogo,
|
||||||
|
$model,
|
||||||
|
$organizationId,
|
||||||
|
(string) ($submittedData['title'] ?? ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (null !== $newLogoUuid) {
|
||||||
|
$this->organizationRepository->updateLogo($organizationId, $newLogoUuid);
|
||||||
|
}
|
||||||
|
} elseif ($deleteLogo) {
|
||||||
|
$this->organizationRepository->updateLogo($organizationId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->request->has('save_back')) {
|
||||||
|
return new RedirectResponse($backUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedirectResponse($request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
$template->set('form', $form->createView());
|
||||||
|
$template->set('backUrl', $backUrl);
|
||||||
|
$template->set('requestToken', $this->container->get('contao.csrf.token_manager')->getDefaultTokenValue());
|
||||||
|
$template->set('currentLogoPath', $currentLogoPath);
|
||||||
|
|
||||||
|
return $template->getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeOrganizationLogo(UploadedFile $uploadedFile, ModuleModel $model, int $organizationId, string $organizationTitle): ?string
|
||||||
|
{
|
||||||
|
if (empty($model->logoFolder)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$folderModel = FilesModel::findByUuid((string) $model->logoFolder);
|
||||||
|
|
||||||
|
if (null === $folderModel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadBasePath = trim((string) $folderModel->path, '/');
|
||||||
|
|
||||||
|
if ('' === $uploadBasePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$titleSlug = strtolower(StringUtil::generateAlias($organizationTitle));
|
||||||
|
$titleSlug = preg_replace('/[^a-z0-9-]+/', '-', $titleSlug ?? '') ?? '';
|
||||||
|
$titleSlug = trim($titleSlug, '-');
|
||||||
|
$titleSlug = substr($titleSlug, 0, 12);
|
||||||
|
|
||||||
|
if ('' === $titleSlug) {
|
||||||
|
$titleSlug = 'organization';
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetRelativeDir = sprintf('%s/org-%d-%s', $uploadBasePath, $organizationId, $titleSlug);
|
||||||
|
$projectDir = (string) $this->getParameter('kernel.project_dir');
|
||||||
|
$targetAbsoluteDir = rtrim($projectDir, '/').'/'.$targetRelativeDir;
|
||||||
|
|
||||||
|
if (!is_dir($targetAbsoluteDir) && !mkdir($targetAbsoluteDir, 0775, true) && !is_dir($targetAbsoluteDir)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||||
|
$safeName = preg_replace('/[^a-zA-Z0-9_-]+/', '-', (string) $originalName);
|
||||||
|
$safeName = trim((string) $safeName, '-');
|
||||||
|
|
||||||
|
if ('' === $safeName) {
|
||||||
|
$safeName = 'logo';
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = $uploadedFile->guessExtension() ?: $uploadedFile->getClientOriginalExtension() ?: 'bin';
|
||||||
|
$filename = sprintf('%s-%s.%s', $safeName, uniqid('', true), strtolower((string) $extension));
|
||||||
|
|
||||||
|
$uploadedFile->move($targetAbsoluteDir, $filename);
|
||||||
|
|
||||||
|
$relativePath = $targetRelativeDir.'/'.$filename;
|
||||||
|
$fileModel = Dbafs::addResource($relativePath);
|
||||||
|
|
||||||
|
return null !== $fileModel ? (string) $fileModel->uuid : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveBackUrl(Request $request, ModuleModel $model): string
|
||||||
|
{
|
||||||
|
if ((int) ($model->listPage ?? 0) > 0) {
|
||||||
|
$listPage = $this->getContaoAdapter(PageModel::class)->findById((int) $model->listPage);
|
||||||
|
|
||||||
|
if ($listPage instanceof PageModel) {
|
||||||
|
return $this->generateContentUrl($listPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = (string) $request->query->get('ref', '');
|
||||||
|
|
||||||
|
if ('' !== $ref) {
|
||||||
|
$decoded = base64_decode($ref, true);
|
||||||
|
|
||||||
|
if (false !== $decoded && str_starts_with($decoded, '/')) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = $this->getPageModel();
|
||||||
|
|
||||||
|
return $page instanceof PageModel ? $this->generateContentUrl($page) : '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\DependencyInjection;
|
||||||
|
|
||||||
|
use Symfony\Component\Config\FileLocator;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||||
|
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
|
||||||
|
|
||||||
|
class MummertMediaEventManagerExtension extends Extension
|
||||||
|
{
|
||||||
|
public function load(array $configs, ContainerBuilder $container): void
|
||||||
|
{
|
||||||
|
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
|
||||||
|
$loader->load('services.yaml');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\EventListener\DataContainer;
|
||||||
|
|
||||||
|
use Contao\DataContainer;
|
||||||
|
|
||||||
|
class SetDateAddedCallback
|
||||||
|
{
|
||||||
|
public function onBeforeSubmit(array $values, DataContainer $dc): array
|
||||||
|
{
|
||||||
|
if (null !== $dc->activeRecord && (int) ($dc->activeRecord->dateAdded ?? 0) > 0) {
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($values['dateAdded']) || 0 === (int) $values['dateAdded']) {
|
||||||
|
$values['dateAdded'] = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TimeType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Url;
|
||||||
|
|
||||||
|
class EventType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$termsLabel = 'Ich stimme den Nutzungsbedingungen zu.';
|
||||||
|
|
||||||
|
if (null !== $options['terms_page_url']) {
|
||||||
|
$termsLabel = sprintf(
|
||||||
|
'Ich stimme den <a href="%s" target="_blank" rel="noopener noreferrer">Nutzungsbedingungen</a> zu.',
|
||||||
|
htmlspecialchars($options['terms_page_url'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$showOrganizationSelect = true === $options['show_organization'];
|
||||||
|
|
||||||
|
$builder
|
||||||
|
->add('title', TextType::class, [
|
||||||
|
'label' => 'Titel',
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('startDate', DateType::class, [
|
||||||
|
'label' => 'Startdatum',
|
||||||
|
'required' => false,
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'input' => 'string',
|
||||||
|
'html5' => false,
|
||||||
|
'format' => 'yyyy-MM-dd',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-flatpickr-date',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('endDate', DateType::class, [
|
||||||
|
'label' => 'Enddatum',
|
||||||
|
'required' => false,
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'input' => 'string',
|
||||||
|
'html5' => false,
|
||||||
|
'format' => 'yyyy-MM-dd',
|
||||||
|
'help' => 'Lassen Sie das Feld leer, um ein eintägiges Event zu erstellen.',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-flatpickr-date',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('addTime', CheckboxType::class, [
|
||||||
|
'label' => 'Dem Event eine Start- und Endzeit hinzufügen.',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-add-time-toggle',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('startTime', TimeType::class, [
|
||||||
|
'label' => 'Startzeit',
|
||||||
|
'required' => false,
|
||||||
|
'input' => 'string',
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'html5' => false,
|
||||||
|
'input_format' => 'H:i',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-flatpickr-time',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('endTime', TimeType::class, [
|
||||||
|
'label' => 'Endzeit',
|
||||||
|
'required' => false,
|
||||||
|
'input' => 'string',
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'html5' => false,
|
||||||
|
'input_format' => 'H:i',
|
||||||
|
'help' => 'Lassen Sie das Feld leer, um ein Event mit offenem Ende zu erstellen.',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-flatpickr-time',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($showOrganizationSelect) {
|
||||||
|
$builder->add('organization_ids', ChoiceType::class, [
|
||||||
|
'label' => 'Veranstalter',
|
||||||
|
'choices' => $options['organization_choices'],
|
||||||
|
'required' => true,
|
||||||
|
'mapped' => false,
|
||||||
|
'multiple' => true,
|
||||||
|
'data' => $options['selected_organization_ids'],
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-organization-choice',
|
||||||
|
'data-placeholder' => 'Veranstalter suchen …',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder
|
||||||
|
->add('location_id', ChoiceType::class, [
|
||||||
|
'label' => 'Veranstaltungsort',
|
||||||
|
'choices' => $options['location_choices'],
|
||||||
|
'required' => false,
|
||||||
|
'choice_value' => static fn ($value) => null !== $value ? (string) $value : '',
|
||||||
|
'placeholder' => 'Bitte auswählen',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-location-choice',
|
||||||
|
'data-placeholder' => 'Veranstaltungsort suchen …',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('type', ChoiceType::class, [
|
||||||
|
'label' => 'Typ',
|
||||||
|
'choices' => [
|
||||||
|
'Unterkunft' => 'accommodation',
|
||||||
|
'Einkaufen' => 'shopping',
|
||||||
|
'Kultur' => 'culture',
|
||||||
|
],
|
||||||
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-event-type-choice',
|
||||||
|
'data-placeholder' => 'Typen suchen …',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('teaser', TextareaType::class, ['label' => 'Teaser', 'required' => false])
|
||||||
|
->add('description', TextareaType::class, ['label' => 'Beschreibung', 'required' => false])
|
||||||
|
->add('url', UrlType::class, [
|
||||||
|
'label' => 'Link (extern)',
|
||||||
|
'required' => false,
|
||||||
|
'default_protocol' => 'https',
|
||||||
|
'invalid_message' => 'Bitte eine gültige URL mit http:// oder https:// eingeben (z. B. https://example.org).',
|
||||||
|
'constraints' => [
|
||||||
|
new Url([
|
||||||
|
'protocols' => ['http', 'https'],
|
||||||
|
'message' => 'Bitte eine gültige URL mit http:// oder https:// eingeben (z. B. https://example.org).',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('addImage', CheckboxType::class, [
|
||||||
|
'label' => 'Dem Event ein Bild hinzufügen',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-add-image-toggle',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('eventUpload', FileType::class, [
|
||||||
|
'label' => 'Bild hochladen',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-event-image-upload',
|
||||||
|
'accept' => 'image/*',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('photographer', TextType::class, [
|
||||||
|
'label' => 'Urheber/Fotograf',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-photographer',
|
||||||
|
'maxlength' => '255',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('termsAccepted', CheckboxType::class, [
|
||||||
|
'label' => $termsLabel,
|
||||||
|
'label_html' => true,
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('isSoldOut', CheckboxType::class, [
|
||||||
|
'label' => 'Diese Veranstaltung ist ausverkauft.',
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
->add('isCanceled', CheckboxType::class, [
|
||||||
|
'label' => 'Diese Veranstaltung wurde abgesagt.',
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
->add('published', CheckboxType::class, ['label' => 'Veröffentlicht', 'required' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'location_choices' => [],
|
||||||
|
'organization_choices' => [],
|
||||||
|
'selected_organization_ids' => [],
|
||||||
|
'show_organization' => false,
|
||||||
|
'terms_page_url' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setAllowedTypes('location_choices', 'array');
|
||||||
|
$resolver->setAllowedTypes('organization_choices', 'array');
|
||||||
|
$resolver->setAllowedTypes('selected_organization_ids', 'array');
|
||||||
|
$resolver->setAllowedTypes('show_organization', 'bool');
|
||||||
|
$resolver->setAllowedTypes('terms_page_url', ['null', 'string']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
class OrganizationType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('title', TextType::class, ['label' => 'Titel', 'required' => true])
|
||||||
|
->add('street', TextType::class, ['label' => 'Straße', 'required' => false])
|
||||||
|
->add('postal', TextType::class, ['label' => 'PLZ', 'required' => false])
|
||||||
|
->add('city', TextType::class, ['label' => 'Ort', 'required' => false])
|
||||||
|
->add('state', TextType::class, ['label' => 'Bundesland', 'required' => false])
|
||||||
|
->add('country', TextType::class, ['label' => 'Land', 'required' => false])
|
||||||
|
->add('phone', TextType::class, ['label' => 'Telefon', 'required' => false])
|
||||||
|
->add('email', EmailType::class, ['label' => 'E-Mail', 'required' => false])
|
||||||
|
->add('website', TextType::class, ['label' => 'Webseite', 'required' => false])
|
||||||
|
->add('description', TextareaType::class, ['label' => 'Beschreibung', 'required' => false])
|
||||||
|
->add('type', ChoiceType::class, [
|
||||||
|
'label' => 'Typ',
|
||||||
|
'choices' => [
|
||||||
|
'Unterkunft' => 'accommodation',
|
||||||
|
'Einkaufen' => 'shopping',
|
||||||
|
'Kultur' => 'culture',
|
||||||
|
],
|
||||||
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-organization-type-choice',
|
||||||
|
'data-placeholder' => 'Typ auswählen …',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('logoUpload', FileType::class, [
|
||||||
|
'label' => 'Logo hochladen',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'js-logo-upload',
|
||||||
|
'accept' => 'image/*',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Model;
|
||||||
|
|
||||||
|
use Contao\Model;
|
||||||
|
|
||||||
|
class LocationModel extends Model
|
||||||
|
{
|
||||||
|
protected static $strTable = 'tl_location';
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Model;
|
||||||
|
|
||||||
|
use Contao\Model;
|
||||||
|
|
||||||
|
class OrganizationModel extends Model
|
||||||
|
{
|
||||||
|
protected static $strTable = 'tl_organization';
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||||
|
|
||||||
|
class MummertMediaEventManagerBundle extends Bundle
|
||||||
|
{
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return dirname(__DIR__);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
|
||||||
|
class EventRepository
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Connection $connection,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function findByMemberId(int $memberId): array
|
||||||
|
{
|
||||||
|
return $this->connection->createQueryBuilder()
|
||||||
|
->select('DISTINCT e.*')
|
||||||
|
->from('tl_calendar_events', 'e')
|
||||||
|
->innerJoin('e', 'tl_calendar_events_organization', 'ceo', 'ceo.event_id = e.id')
|
||||||
|
->innerJoin('ceo', 'tl_member_organization', 'mo', 'mo.organization_id = ceo.organization_id')
|
||||||
|
->where('mo.member_id = :memberId')
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->orderBy('e.startDate', 'DESC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function memberHasEvent(int $memberId, int $eventId): bool
|
||||||
|
{
|
||||||
|
$exists = $this->connection->createQueryBuilder()
|
||||||
|
->select('1')
|
||||||
|
->from('tl_calendar_events', 'e')
|
||||||
|
->innerJoin('e', 'tl_calendar_events_organization', 'ceo', 'ceo.event_id = e.id')
|
||||||
|
->innerJoin('ceo', 'tl_member_organization', 'mo', 'mo.organization_id = ceo.organization_id')
|
||||||
|
->where('e.id = :eventId')
|
||||||
|
->andWhere('mo.member_id = :memberId')
|
||||||
|
->setParameter('eventId', $eventId, ParameterType::INTEGER)
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchOne();
|
||||||
|
|
||||||
|
return false !== $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function togglePublished(int $eventId): void
|
||||||
|
{
|
||||||
|
$published = $this->connection->createQueryBuilder()
|
||||||
|
->select('published')
|
||||||
|
->from('tl_calendar_events')
|
||||||
|
->where('id = :id')
|
||||||
|
->setParameter('id', $eventId, ParameterType::INTEGER)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchOne();
|
||||||
|
|
||||||
|
$isPublished = '1' === (string) $published;
|
||||||
|
|
||||||
|
$this->connection->update(
|
||||||
|
'tl_calendar_events',
|
||||||
|
[
|
||||||
|
'published' => $isPublished ? 0 : 1,
|
||||||
|
'tstamp' => time(),
|
||||||
|
],
|
||||||
|
['id' => $eventId],
|
||||||
|
['id' => ParameterType::INTEGER],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function duplicate(int $eventId): ?int
|
||||||
|
{
|
||||||
|
$row = $this->findById($eventId);
|
||||||
|
|
||||||
|
if (null === $row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($row['id']);
|
||||||
|
|
||||||
|
if (array_key_exists('title', $row)) {
|
||||||
|
$row['title'] = trim((string) $row['title'].' (Kopie)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('alias', $row)) {
|
||||||
|
$row['alias'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('published', $row)) {
|
||||||
|
$row['published'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if (array_key_exists('dateAdded', $row)) {
|
||||||
|
$row['dateAdded'] = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('tstamp', $row)) {
|
||||||
|
$row['tstamp'] = $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->insert('tl_calendar_events', $row);
|
||||||
|
|
||||||
|
$newEventId = (int) $this->connection->lastInsertId();
|
||||||
|
|
||||||
|
$organizationIds = $this->connection->createQueryBuilder()
|
||||||
|
->select('organization_id')
|
||||||
|
->from('tl_calendar_events_organization')
|
||||||
|
->where('event_id = :eventId')
|
||||||
|
->setParameter('eventId', $eventId, ParameterType::INTEGER)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchFirstColumn();
|
||||||
|
|
||||||
|
foreach ($organizationIds as $organizationId) {
|
||||||
|
$this->connection->insert(
|
||||||
|
'tl_calendar_events_organization',
|
||||||
|
[
|
||||||
|
'event_id' => $newEventId,
|
||||||
|
'organization_id' => (int) $organizationId,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'event_id' => ParameterType::INTEGER,
|
||||||
|
'organization_id' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newEventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDraftForMember(int $memberId, int $authorId, int $archiveId): ?int
|
||||||
|
{
|
||||||
|
$resolvedArchiveId = $this->resolveArchiveId($archiveId);
|
||||||
|
|
||||||
|
if ($resolvedArchiveId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$organizationId = $this->connection->createQueryBuilder()
|
||||||
|
->select('organization_id')
|
||||||
|
->from('tl_member_organization')
|
||||||
|
->where('member_id = :memberId')
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->orderBy('organization_id', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchOne();
|
||||||
|
|
||||||
|
if (false === $organizationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$this->connection->insert(
|
||||||
|
'tl_calendar_events',
|
||||||
|
[
|
||||||
|
'pid' => $resolvedArchiveId,
|
||||||
|
'author' => max(0, $authorId),
|
||||||
|
'title' => '',
|
||||||
|
'alias' => '',
|
||||||
|
'startDate' => 0,
|
||||||
|
'endDate' => null,
|
||||||
|
'published' => 0,
|
||||||
|
'tstamp' => $now,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pid' => ParameterType::INTEGER,
|
||||||
|
'author' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$eventId = (int) $this->connection->lastInsertId();
|
||||||
|
|
||||||
|
if ($eventId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->insert(
|
||||||
|
'tl_calendar_events_organization',
|
||||||
|
[
|
||||||
|
'tstamp' => $now,
|
||||||
|
'event_id' => $eventId,
|
||||||
|
'organization_id' => (int) $organizationId,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'event_id' => ParameterType::INTEGER,
|
||||||
|
'organization_id' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{authorId:int,archiveId:int} */
|
||||||
|
public function findCreationDefaultsByListPage(int $listPageId): array
|
||||||
|
{
|
||||||
|
if ($listPageId <= 0) {
|
||||||
|
return ['authorId' => 0, 'archiveId' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->connection->createQueryBuilder()
|
||||||
|
->select('frontendAuthorId', 'frontendArchiveId')
|
||||||
|
->from('tl_module')
|
||||||
|
->where('type = :type')
|
||||||
|
->andWhere('listPage = :listPage')
|
||||||
|
->setParameter('type', 'event_edit')
|
||||||
|
->setParameter('listPage', $listPageId, ParameterType::INTEGER)
|
||||||
|
->orderBy('id', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (false === $row || null === $row) {
|
||||||
|
return ['authorId' => 0, 'archiveId' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'authorId' => (int) ($row['frontendAuthorId'] ?? 0),
|
||||||
|
'archiveId' => (int) ($row['frontendArchiveId'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveArchiveId(int $archiveId): int
|
||||||
|
{
|
||||||
|
if ($archiveId > 0) {
|
||||||
|
$exists = $this->connection->createQueryBuilder()
|
||||||
|
->select('id')
|
||||||
|
->from('tl_calendar')
|
||||||
|
->where('id = :id')
|
||||||
|
->setParameter('id', $archiveId, ParameterType::INTEGER)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchOne();
|
||||||
|
|
||||||
|
if (false !== $exists) {
|
||||||
|
return $archiveId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback = $this->connection->createQueryBuilder()
|
||||||
|
->select('id')
|
||||||
|
->from('tl_calendar')
|
||||||
|
->orderBy('id', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchOne();
|
||||||
|
|
||||||
|
return false !== $fallback ? (int) $fallback : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $eventId): void
|
||||||
|
{
|
||||||
|
$this->connection->delete(
|
||||||
|
'tl_calendar_events_organization',
|
||||||
|
['event_id' => $eventId],
|
||||||
|
['event_id' => ParameterType::INTEGER],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->connection->delete(
|
||||||
|
'tl_calendar_events',
|
||||||
|
['id' => $eventId],
|
||||||
|
['id' => ParameterType::INTEGER],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed>|null */
|
||||||
|
public function findById(int $eventId): ?array
|
||||||
|
{
|
||||||
|
$row = $this->connection->createQueryBuilder()
|
||||||
|
->select('*')
|
||||||
|
->from('tl_calendar_events')
|
||||||
|
->where('id = :id')
|
||||||
|
->setParameter('id', $eventId, ParameterType::INTEGER)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
return false === $row ? null : $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{id:int,title:string}|null */
|
||||||
|
public function findPrimaryOrganizationForEvent(int $eventId): ?array
|
||||||
|
{
|
||||||
|
$row = $this->connection->createQueryBuilder()
|
||||||
|
->select('o.id', 'o.title')
|
||||||
|
->from('tl_calendar_events_organization', 'ceo')
|
||||||
|
->innerJoin('ceo', 'tl_organization', 'o', 'o.id = ceo.organization_id')
|
||||||
|
->where('ceo.event_id = :eventId')
|
||||||
|
->setParameter('eventId', $eventId, ParameterType::INTEGER)
|
||||||
|
->orderBy('ceo.organization_id', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
if (false === $row || null === $row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $row['id'],
|
||||||
|
'title' => (string) ($row['title'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int> */
|
||||||
|
public function getOrganizationIdsForEvent(int $eventId): array
|
||||||
|
{
|
||||||
|
$rows = $this->connection->createQueryBuilder()
|
||||||
|
->select('organization_id')
|
||||||
|
->from('tl_calendar_events_organization')
|
||||||
|
->where('event_id = :eventId')
|
||||||
|
->setParameter('eventId', $eventId, ParameterType::INTEGER)
|
||||||
|
->orderBy('organization_id', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchFirstColumn();
|
||||||
|
|
||||||
|
return array_values(array_unique(array_map('intval', $rows)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
public function getLocationChoices(): array
|
||||||
|
{
|
||||||
|
$rows = $this->connection->createQueryBuilder()
|
||||||
|
->select('id', 'title')
|
||||||
|
->from('tl_location')
|
||||||
|
->where('published = :published')
|
||||||
|
->setParameter('published', '1')
|
||||||
|
->orderBy('title', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$choices = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$choices[(string) $row['title']] = (int) $row['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
public function getOrganizationChoicesForMember(int $memberId): array
|
||||||
|
{
|
||||||
|
$rows = $this->connection->createQueryBuilder()
|
||||||
|
->select('o.id', 'o.title')
|
||||||
|
->from('tl_member_organization', 'mo')
|
||||||
|
->innerJoin('mo', 'tl_organization', 'o', 'o.id = mo.organization_id')
|
||||||
|
->where('mo.member_id = :memberId')
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->orderBy('o.title', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
$choices = [];
|
||||||
|
$seenLabels = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$id = (int) $row['id'];
|
||||||
|
$label = trim((string) ($row['title'] ?? ''));
|
||||||
|
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = 'Organisation '.$id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($seenLabels[$label])) {
|
||||||
|
$label .= ' (#'.$id.')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$seenLabels[$label] = true;
|
||||||
|
$choices[$label] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int> */
|
||||||
|
public function getOrganizationIdsForMember(int $memberId): array
|
||||||
|
{
|
||||||
|
$ids = $this->connection->createQueryBuilder()
|
||||||
|
->select('organization_id')
|
||||||
|
->from('tl_member_organization')
|
||||||
|
->where('member_id = :memberId')
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->orderBy('organization_id', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchFirstColumn();
|
||||||
|
|
||||||
|
return array_values(array_unique(array_map('intval', $ids)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int> $organizationIds */
|
||||||
|
public function assignEventToOrganizations(int $eventId, array $organizationIds): void
|
||||||
|
{
|
||||||
|
$this->connection->delete(
|
||||||
|
'tl_calendar_events_organization',
|
||||||
|
['event_id' => $eventId],
|
||||||
|
['event_id' => ParameterType::INTEGER],
|
||||||
|
);
|
||||||
|
|
||||||
|
$organizationIds = array_values(array_unique(array_map('intval', $organizationIds)));
|
||||||
|
|
||||||
|
foreach ($organizationIds as $organizationId) {
|
||||||
|
if ($organizationId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->insert(
|
||||||
|
'tl_calendar_events_organization',
|
||||||
|
[
|
||||||
|
'tstamp' => time(),
|
||||||
|
'event_id' => $eventId,
|
||||||
|
'organization_id' => $organizationId,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'event_id' => ParameterType::INTEGER,
|
||||||
|
'organization_id' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
public function update(int $eventId, array $data): void
|
||||||
|
{
|
||||||
|
$eventData = $this->buildEventData($data);
|
||||||
|
|
||||||
|
$this->connection->update(
|
||||||
|
'tl_calendar_events',
|
||||||
|
$eventData,
|
||||||
|
['id' => $eventId],
|
||||||
|
[
|
||||||
|
'id' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @param array<int> $organizationIds
|
||||||
|
*/
|
||||||
|
public function createForMember(int $memberId, int $authorId, int $archiveId, array $data, array $organizationIds): ?int
|
||||||
|
{
|
||||||
|
if ($memberId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedArchiveId = $this->resolveArchiveId($archiveId);
|
||||||
|
|
||||||
|
if ($resolvedArchiveId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventData = $this->buildEventData($data);
|
||||||
|
$eventData['pid'] = $resolvedArchiveId;
|
||||||
|
$eventData['author'] = max(0, $authorId);
|
||||||
|
|
||||||
|
$this->connection->insert(
|
||||||
|
'tl_calendar_events',
|
||||||
|
$eventData,
|
||||||
|
[
|
||||||
|
'pid' => ParameterType::INTEGER,
|
||||||
|
'author' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$eventId = (int) $this->connection->lastInsertId();
|
||||||
|
|
||||||
|
if ($eventId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$memberOrganizationIds = $this->getOrganizationIdsForMember($memberId);
|
||||||
|
$allowedOrganizationIds = array_values(array_intersect(array_map('intval', $organizationIds), array_map('intval', $memberOrganizationIds)));
|
||||||
|
|
||||||
|
if ([] === $allowedOrganizationIds) {
|
||||||
|
$this->connection->delete('tl_calendar_events', ['id' => $eventId], ['id' => ParameterType::INTEGER]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assignEventToOrganizations($eventId, $allowedOrganizationIds);
|
||||||
|
|
||||||
|
return $eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildEventData(array $data): array
|
||||||
|
{
|
||||||
|
$startDate = !empty($data['startDate']) ? strtotime((string) $data['startDate']) : false;
|
||||||
|
$endDate = !empty($data['endDate']) ? strtotime((string) $data['endDate']) : false;
|
||||||
|
$url = trim((string) ($data['url'] ?? ''));
|
||||||
|
$addTime = !empty($data['addTime']);
|
||||||
|
$alias = $this->buildAlias($startDate, (string) ($data['title'] ?? ''));
|
||||||
|
|
||||||
|
$startDateTimestamp = false !== $startDate ? (int) $startDate : 0;
|
||||||
|
$endDateTimestamp = false !== $endDate ? (int) $endDate : 0;
|
||||||
|
$timeBaseTimestamp = $endDateTimestamp > 0 ? $endDateTimestamp : $startDateTimestamp;
|
||||||
|
|
||||||
|
$startTimeTimestamp = $startDateTimestamp;
|
||||||
|
$endTimeTimestamp = $timeBaseTimestamp > 0 ? $timeBaseTimestamp + 86399 : 0;
|
||||||
|
|
||||||
|
if ($addTime) {
|
||||||
|
$startTimeCandidate = $this->combineDateAndTime($startDateTimestamp, (string) ($data['startTime'] ?? ''));
|
||||||
|
$startTimeTimestamp = null !== $startTimeCandidate ? $startTimeCandidate : $startDateTimestamp;
|
||||||
|
|
||||||
|
$endTimeCandidate = $this->combineDateAndTime($timeBaseTimestamp, (string) ($data['endTime'] ?? ''));
|
||||||
|
$endTimeTimestamp = null !== $endTimeCandidate ? $endTimeCandidate : $startTimeTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $data['title'] ?? '',
|
||||||
|
'alias' => $alias,
|
||||||
|
'startDate' => false !== $startDate ? $startDate : 0,
|
||||||
|
'endDate' => false !== $endDate ? $endDate : null,
|
||||||
|
'location_id' => (int) ($data['location_id'] ?? 0),
|
||||||
|
'addTime' => $addTime ? 1 : 0,
|
||||||
|
'startTime' => $startTimeTimestamp,
|
||||||
|
'endTime' => $endTimeTimestamp,
|
||||||
|
'type' => serialize($data['type'] ?? []),
|
||||||
|
'teaser' => $data['teaser'] ?? null,
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
'url' => $url,
|
||||||
|
'source' => '' !== $url ? 'external' : 'default',
|
||||||
|
'photographer' => $data['photographer'] ?? '',
|
||||||
|
'addImage' => !empty($data['addImage']) ? 1 : 0,
|
||||||
|
'termsAccepted' => !empty($data['termsAccepted']) ? '1' : '',
|
||||||
|
'isSoldOut' => !empty($data['isSoldOut']) ? '1' : '',
|
||||||
|
'isCanceled' => !empty($data['isCanceled']) ? '1' : '',
|
||||||
|
'published' => !empty($data['published']) ? 1 : 0,
|
||||||
|
'tstamp' => time(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildAlias(false|int $startDate, string $title): string
|
||||||
|
{
|
||||||
|
if (false === $startDate || $startDate <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedTitle = trim($title);
|
||||||
|
|
||||||
|
if ('' === $normalizedTitle) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedTitle = strtolower($normalizedTitle);
|
||||||
|
$transliterated = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalizedTitle);
|
||||||
|
|
||||||
|
if (false !== $transliterated) {
|
||||||
|
$normalizedTitle = strtolower($transliterated);
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedTitle = preg_replace('/[^a-z0-9]+/', '', $normalizedTitle) ?? '';
|
||||||
|
$normalizedTitle = substr($normalizedTitle, 0, 20);
|
||||||
|
|
||||||
|
if ('' === $normalizedTitle) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d', $startDate).'_'.$normalizedTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function combineDateAndTime(int $dateTimestamp, string $time): ?int
|
||||||
|
{
|
||||||
|
if ($dateTimestamp <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedTime = trim($time);
|
||||||
|
|
||||||
|
if (!preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $normalizedTime, $matches)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hours = (int) $matches[1];
|
||||||
|
$minutes = (int) $matches[2];
|
||||||
|
|
||||||
|
return $dateTimestamp + ($hours * 3600) + ($minutes * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateImageFields(int $eventId, bool $addImage, ?string $singleSrcUuid): void
|
||||||
|
{
|
||||||
|
$types = [
|
||||||
|
'id' => ParameterType::INTEGER,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (null !== $singleSrcUuid) {
|
||||||
|
$types['singleSRC'] = ParameterType::BINARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->update(
|
||||||
|
'tl_calendar_events',
|
||||||
|
[
|
||||||
|
'addImage' => $addImage ? 1 : 0,
|
||||||
|
'singleSRC' => $singleSrcUuid,
|
||||||
|
'tstamp' => time(),
|
||||||
|
],
|
||||||
|
['id' => $eventId],
|
||||||
|
$types,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MummertMedia\EventManagerBundle\Service;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
|
||||||
|
class OrganizationRepository
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Connection $connection,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function findByMemberId(int $memberId): array
|
||||||
|
{
|
||||||
|
return $this->connection->createQueryBuilder()
|
||||||
|
->select('o.*')
|
||||||
|
->from('tl_organization', 'o')
|
||||||
|
->innerJoin('o', 'tl_member_organization', 'mo', 'mo.organization_id = o.id')
|
||||||
|
->where('mo.member_id = :memberId')
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->orderBy('o.title', 'ASC')
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function memberHasOrganization(int $memberId, int $organizationId): bool
|
||||||
|
{
|
||||||
|
$exists = $this->connection->createQueryBuilder()
|
||||||
|
->select('1')
|
||||||
|
->from('tl_member_organization', 'mo')
|
||||||
|
->where('mo.member_id = :memberId')
|
||||||
|
->andWhere('mo.organization_id = :organizationId')
|
||||||
|
->setParameter('memberId', $memberId, ParameterType::INTEGER)
|
||||||
|
->setParameter('organizationId', $organizationId, ParameterType::INTEGER)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchOne();
|
||||||
|
|
||||||
|
return false !== $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed>|null */
|
||||||
|
public function findById(int $organizationId): ?array
|
||||||
|
{
|
||||||
|
$row = $this->connection->createQueryBuilder()
|
||||||
|
->select('*')
|
||||||
|
->from('tl_organization')
|
||||||
|
->where('id = :id')
|
||||||
|
->setParameter('id', $organizationId, ParameterType::INTEGER)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
|
||||||
|
return false === $row ? null : $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
public function update(int $organizationId, array $data): void
|
||||||
|
{
|
||||||
|
$this->connection->update(
|
||||||
|
'tl_organization',
|
||||||
|
[
|
||||||
|
'title' => $data['title'] ?? '',
|
||||||
|
'street' => $data['street'] ?? '',
|
||||||
|
'postal' => $data['postal'] ?? '',
|
||||||
|
'city' => $data['city'] ?? '',
|
||||||
|
'state' => $data['state'] ?? '',
|
||||||
|
'country' => $data['country'] ?? '',
|
||||||
|
'phone' => $data['phone'] ?? '',
|
||||||
|
'email' => $data['email'] ?? '',
|
||||||
|
'website' => $data['website'] ?? '',
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
'type' => serialize($data['type'] ?? []),
|
||||||
|
'tstamp' => time(),
|
||||||
|
],
|
||||||
|
['id' => $organizationId],
|
||||||
|
[
|
||||||
|
'id' => ParameterType::INTEGER,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLogo(int $organizationId, ?string $logoUuid): void
|
||||||
|
{
|
||||||
|
$types = [
|
||||||
|
'id' => ParameterType::INTEGER,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (null !== $logoUuid) {
|
||||||
|
$types['logo'] = ParameterType::BINARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->update(
|
||||||
|
'tl_organization',
|
||||||
|
[
|
||||||
|
'logo' => $logoUuid,
|
||||||
|
'tstamp' => time(),
|
||||||
|
],
|
||||||
|
['id' => $organizationId],
|
||||||
|
$types,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user