Initial bundle implementation
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,98 @@
|
||||
# gymnasium-nossen-bundle
|
||||
|
||||
Contao 5.7 Bundle für eine gefilterte Eventliste mit PDF-Export.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Frontend-Modul `gymnasium_eventlist` (Kategorie `events`)
|
||||
- Zwei-Spalten-Layout im Frontend (links Filter/Export, rechts Eventliste)
|
||||
- Filter über:
|
||||
- Event-Archive (Checkboxen, Multi-Select)
|
||||
- Date-Range `from` und `to` (`YYYY-MM-DD`)
|
||||
- Auto-Filter ohne separaten Submit-Button (bei Datums- oder Archivänderung)
|
||||
- Flatpickr via CDN (Dark Theme) mit Clear-Button pro Datumsfeld
|
||||
- Ausgabe nur erlaubter Archive/Events gemäß Archivschutz
|
||||
- CSS-Klassen pro Event im Markup:
|
||||
- `pid-<id>`
|
||||
- `is-featured` (wenn Event im Backend hervorgehoben)
|
||||
- PDF-Export ausgewählter Events über `/events/pdf`
|
||||
- Optionale PDF-Felder:
|
||||
- Überschrift
|
||||
- Einleitungstext
|
||||
|
||||
## Anforderungen
|
||||
|
||||
- PHP `^8.4`
|
||||
- `contao/core-bundle ^5.7`
|
||||
- `contao/calendar-bundle ^5.7`
|
||||
- `dompdf/dompdf ^3.1`
|
||||
|
||||
## Installation im Hauptprojekt
|
||||
|
||||
Das Bundle wird als `path`-Repository eingebunden und im Hauptprojekt per Composer installiert.
|
||||
|
||||
## Modul-Konfiguration
|
||||
|
||||
Backend-Modultyp:
|
||||
|
||||
- `gefilterte Eventliste` (`gymnasium_eventlist`)
|
||||
|
||||
Einstellungen:
|
||||
|
||||
- `cal_calendar` (Mehrfachauswahl)
|
||||
|
||||
Hinweis zur Archivfilter-Vorauswahl:
|
||||
|
||||
- Erstaufruf: alle erlaubten Archive sind vorausgewählt
|
||||
- Bewusst alle Archive abgewählt: es werden keine Events angezeigt (`Keine Termine gefunden.`)
|
||||
|
||||
## PDF-Export
|
||||
|
||||
Route:
|
||||
|
||||
- `POST /events/pdf`
|
||||
|
||||
POST-Felder:
|
||||
|
||||
- `event_ids[]` (Pflicht)
|
||||
- `from` (optional)
|
||||
- `to` (optional)
|
||||
- `pdf_heading` (optional)
|
||||
- `pdf_intro` (optional)
|
||||
|
||||
PDF-Inhalt:
|
||||
|
||||
- Logo oben rechts (nur erste Seite)
|
||||
- Überschrift (optional überschreibbar)
|
||||
- Einleitungstext (optional)
|
||||
- Datumsbereich (falls gesetzt)
|
||||
- Eventzeilen mit Contao-Zeitlogik (`addTime`, `startDate/endDate`, `startTime/endTime`)
|
||||
- Datum/Uhrzeit in Dunkelgrau und kursiv
|
||||
|
||||
## Berechtigungslogik (Archive)
|
||||
|
||||
- Nicht eingeloggte Nutzer sehen nur ungeschützte Event-Archive.
|
||||
- Eingeloggte `tl_member` sehen zusätzlich geschützte Archive, wenn ihre Mitgliedsgruppen in den Archivgruppen enthalten sind.
|
||||
- Diese Logik gilt sowohl für den Archiv-Filter als auch für die Eventliste.
|
||||
|
||||
## Aktive Struktur (final)
|
||||
|
||||
- `contao/config/modules.php`
|
||||
- `contao/dca/tl_module.php`
|
||||
- `contao/languages/de/modules.php`
|
||||
- `contao/languages/en/modules.php`
|
||||
- `contao/templates/frontend/module_event_list.html.twig`
|
||||
- `src/Contao/Manager/Plugin.php`
|
||||
- `src/Controller/EventListModuleController.php`
|
||||
- `src/Controller/EventPdfController.php`
|
||||
- `src/DependencyInjection/GymnasiumNossenExtension.php`
|
||||
- `src/Resources/config/routes.yaml`
|
||||
- `src/Resources/contao/config/services.yaml`
|
||||
- `Resources/views/frontend/module_event_list.html.twig`
|
||||
- `Resources/views/pdf/events.html.twig`
|
||||
- `Resources/img/2022_06_26_Gymnasium_Nossen_Siegel_schwarz.svg`
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Die primären Twig-Dateien für das Runtime-Rendering liegen unter `Resources/views/...`.
|
||||
- Nach Änderungen: `cache:clear` ausführen.
|
||||
@@ -0,0 +1,274 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="243.78px" height="243.78px" viewBox="0 0 243.78 243.78" enable-background="new 0 0 243.78 243.78" xml:space="preserve">
|
||||
<path d="M121.837,239.983c-65.353,0-118.521-53.168-118.521-118.52c0-65.352,53.168-118.52,118.521-118.52
|
||||
c65.352,0,118.52,53.167,118.52,118.52C240.357,186.815,187.189,239.983,121.837,239.983z M121.837,6.944
|
||||
c-63.147,0-114.521,51.373-114.521,114.52s51.374,114.52,114.521,114.52c63.147,0,114.52-51.373,114.52-114.52
|
||||
S184.984,6.944,121.837,6.944z"/>
|
||||
<g>
|
||||
<path d="M39.209,173.354c1.081,0.812,2.621,2.19,3.458,3.362c1.011,1.414,1.885,3.007,1.91,4.889
|
||||
c0.005,1.817-1.108,3.879-3.257,5.414c-3.896,2.783-7.743,2.312-10.676-1.793c-1.173-1.643-1.697-3.115-1.91-4.151l2.743-1.96
|
||||
l0.399,0.56c-1.782,1.827-1.777,3.46-0.466,5.294c2.296,3.215,5.681,2.117,8.25,0.281c1.992-1.422,4.954-4.516,2.283-8.254
|
||||
c-0.387-0.542-0.799-0.934-1.256-1.24c-0.359-0.245-0.701-0.317-1.26,0.082l-2.324,1.66c-0.804,0.574-0.829,0.724,0.222,2.453
|
||||
l-0.542,0.387l-3.308-4.629l0.542-0.387c0.896,0.995,1.016,1.016,1.837,0.429L39.209,173.354z"/>
|
||||
<path d="M27.007,175.24c-1.008,0.528-1.026,0.586-0.304,2.195l-0.589,0.309l-4.772-9.107l2.966-1.554l0.319,0.608
|
||||
c-1.705,1.232-1.971,1.881-0.895,3.935l0.857,1.635c0.299,0.57,0.415,0.606,0.985,0.308l4.297-2.252l-1.226-2.338
|
||||
c-0.787-1.502-1.331-1.338-2.533-0.902l-0.319-0.609l4.031-2.111l0.319,0.607c-1.061,0.75-1.543,1.125-0.756,2.627l1.226,2.338
|
||||
l3.897-2.043c1.217-0.637,1.421-0.986,0.335-3.059c-1.176-2.244-2.051-2.803-4.438-2.086l-0.329-0.627l3.488-1.344l4.962,9.469
|
||||
l-0.589,0.309c-0.912-1.51-0.989-1.518-1.978-1L27.007,175.24z"/>
|
||||
<path d="M20.361,156.779c-1.802,1.057-2.44,2.334-1.827,3.938c0.597,1.564,1.942,2.016,3.325,1.486
|
||||
c1.504-0.574,1.631-2.346,1.645-3.752c-0.003-2.596,0.103-4.543,2.569-5.485c2.005-0.767,4.367-0.175,5.662,3.214
|
||||
c0.505,1.322,0.762,3.018,0.302,4.158l-2.991,1.372l-0.245-0.642c1.961-1.301,2.861-2.977,2.194-4.721
|
||||
c-0.636-1.665-1.929-2.343-3.553-1.723c-1.564,0.598-1.781,2.496-1.761,4.051c0.021,2.221-0.16,4.15-2.506,5.047
|
||||
c-2.586,0.988-4.52-0.524-5.424-2.891c-0.49-1.283-0.635-2.744-0.469-3.635l2.827-1.08L20.361,156.779z"/>
|
||||
<path d="M22.434,140.012l3.442-0.571c0.76,0.827,1.566,2.551,1.957,4.113c1.176,4.707-0.449,7.769-5.24,8.965
|
||||
c-2.208,0.552-4.098,0.338-5.649-0.469c-1.629-0.855-2.822-2.615-3.431-5.052c-0.432-1.729-0.462-3.358-0.249-4.363l3.166-0.791
|
||||
l0.167,0.667c-2.192,0.968-2.792,2.467-2.287,4.487c0.916,3.666,4.514,3.983,7.638,3.204c2.354-0.588,6.181-2.363,5.182-6.362
|
||||
c-0.64-2.562-2.327-3.114-4.524-3.141L22.434,140.012z"/>
|
||||
<path d="M18.83,134.707l4.917-0.459c1.133-0.106,1.172-0.153,1.136-1.918l0.663-0.062l0.536,5.729l-0.663,0.062
|
||||
c-0.292-1.74-0.339-1.779-1.472-1.674l-10.047,0.939c-1.133,0.106-1.172,0.153-1.115,1.916l-0.663,0.062l-0.536-5.729l0.663-0.062
|
||||
c0.269,1.721,0.339,1.779,1.451,1.676l4.318-0.404l-0.644-6.883l-4.318,0.403c-1.133,0.106-1.171,0.152-1.115,1.915l-0.663,0.062
|
||||
l-0.536-5.729l0.663-0.062c0.269,1.721,0.318,1.781,1.451,1.676l10.047-0.939c1.133-0.106,1.17-0.174,1.136-1.918l0.663-0.062
|
||||
l0.536,5.73l-0.663,0.061c-0.292-1.74-0.339-1.779-1.472-1.673l-4.917,0.46L18.83,134.707z"/>
|
||||
<path d="M12.131,102.22l0.663,0.053c-0.015,1.248,0.05,1.511,1.176,1.968c3.42,1.374,8.045,3.038,11.706,4.367l-0.135,1.669
|
||||
c-3.632,0.719-7.401,1.535-11.219,2.412l-0.003,0.042c0,0,10.686,3.94,10.705,3.963l-0.135,1.669
|
||||
c-4.129,0.744-8.175,1.516-12.29,2.347c-1.183,0.249-1.262,0.437-1.421,1.607l-0.663-0.053l0.419-5.199l0.663,0.054l0.005,1.012
|
||||
c0.024,0.497,0.125,0.591,0.8,0.495c3.187-0.54,6.663-1.186,10.147-1.938l0.003-0.043c-0.189-0.058-10.733-3.879-10.922-3.938
|
||||
l0.124-1.54c3.746-0.796,7.67-1.664,11.439-2.48l0.005-0.064c-2.719-0.994-6.906-2.473-9.743-3.347
|
||||
c-0.44-0.144-0.724-0.102-0.806,0.387l-0.229,1.23l-0.663-0.053L12.131,102.22z"/>
|
||||
<path d="M25.446,99.532c1.091,0.235,1.146,0.181,1.638-1.491l0.65,0.14l-1.206,5.602l-0.65-0.14
|
||||
c0.245-1.748,0.211-1.798-0.879-2.033l-9.882-2.127c-1.112-0.239-1.163-0.207-1.638,1.492l-0.65-0.14l1.206-5.602l0.65,0.14
|
||||
c-0.261,1.722-0.232,1.794,0.88,2.033L25.446,99.532z"/>
|
||||
<path d="M20.296,86.326c-2.07-0.288-3.362,0.32-3.875,1.96c-0.5,1.599,0.276,2.786,1.69,3.228c1.537,0.481,2.735-0.832,3.618-1.928
|
||||
c1.605-2.04,2.896-3.504,5.416-2.715c2.049,0.641,3.539,2.569,2.456,6.032c-0.423,1.352-1.271,2.842-2.34,3.453l-3.199-0.776
|
||||
l0.205-0.656c2.346,0.194,4.091-0.565,4.648-2.348c0.532-1.701-0.063-3.034-1.723-3.553c-1.598-0.5-2.945,0.856-3.893,2.089
|
||||
c-1.359,1.757-2.698,3.161-5.096,2.411c-2.644-0.827-3.225-3.213-2.468-5.631c0.41-1.312,1.202-2.549,1.883-3.146l2.89,0.904
|
||||
L20.296,86.326z"/>
|
||||
<path d="M32.01,80.182c0.823,0.368,1.056,0.425,1.607-0.598l0.479-0.914l0.607,0.271l-2.68,5.995l-0.607-0.271l0.342-0.976
|
||||
c0.425-1.056,0.188-1.208-0.616-1.568l-10.325-4.616l-0.473,1.058c-0.946,2.116-0.644,2.65,1.377,3.86l-0.28,0.627l-3.272-1.463
|
||||
l4.8-10.736l3.272,1.462l-0.28,0.627c-2.269-0.708-2.84-0.588-3.795,1.548l-0.482,1.078L32.01,80.182z"/>
|
||||
<path d="M26.741,68.147c-0.973-0.59-1.032-0.576-2.037,0.874l-0.569-0.345l5.335-8.793l2.864,1.738l-0.356,0.587
|
||||
c-1.937-0.823-2.629-0.716-3.833,1.266l-0.958,1.579c-0.334,0.551-0.305,0.668,0.246,1.003l4.148,2.517l1.37-2.258
|
||||
c0.88-1.45,0.458-1.832-0.536-2.635l0.356-0.587l3.892,2.361l-0.356,0.587c-1.19-0.521-1.76-0.742-2.64,0.708l-1.37,2.258
|
||||
l3.763,2.284c1.175,0.713,1.58,0.708,2.794-1.293c1.314-2.166,1.342-3.204-0.506-4.878l0.368-0.606l2.953,2.294l-5.547,9.141
|
||||
l-0.569-0.345c0.822-1.561,0.789-1.631-0.166-2.21L26.741,68.147z"/>
|
||||
<path d="M43.268,61.471c0.874,0.695,0.947,0.671,2.131-0.609l0.521,0.415l-3.583,4.502l-0.521-0.415
|
||||
c0.995-1.458,0.988-1.518,0.115-2.213l-7.913-6.295c-0.891-0.708-0.951-0.702-2.131,0.609l-0.521-0.414l3.89-4.889
|
||||
c2.179-2.739,3.99-2.532,5.485-1.343c1.512,1.203,1.563,3,0.66,4.723l0.033,0.027c1.753-1.376,3.287-0.924,4.845,0.014
|
||||
c1.195,0.705,2.219,1.107,3.329-0.012l0.521,0.415l-1.056,1.327c-0.775,0.974-1.623,1.315-2.834,0.735
|
||||
c-0.856-0.407-2.246-1.211-2.793-1.454c-1.37-0.624-2.3-0.074-3.076,0.9l-0.815,1.025L43.268,61.471z M38.967,58.049l0.936-1.176
|
||||
c1.229-1.545,1.206-2.826-0.44-4.136c-2.1-1.671-3.311-0.494-4.233,0.665c-0.815,1.025-0.698,1.119-0.026,1.653L38.967,58.049z"/>
|
||||
<path d="M50.539,47.749l0.964,0.949l-3.584,3.643l-0.964-0.949L50.539,47.749z"/>
|
||||
<path d="M55.348,37.087c-1.587-1.358-3.003-1.544-4.319-0.44c-1.283,1.076-1.271,2.494-0.319,3.629
|
||||
c1.035,1.233,2.751,0.775,4.086,0.328c2.452-0.852,4.327-1.388,6.024,0.635c1.379,1.645,1.593,4.071-1.187,6.402
|
||||
c-1.085,0.911-2.603,1.707-3.832,1.645l-2.275-2.378l0.526-0.441c1.871,1.429,3.749,1.731,5.18,0.531
|
||||
c1.365-1.145,1.583-2.588,0.466-3.921c-1.076-1.283-2.941-0.868-4.404-0.341c-2.092,0.746-3.977,1.205-5.59-0.719
|
||||
c-1.779-2.122-0.981-4.444,0.96-6.072c1.053-0.883,2.386-1.497,3.282-1.632l1.945,2.319L55.348,37.087z"/>
|
||||
<path d="M72.255,33.341l1.654,3.071c-0.538,0.986-1.907,2.307-3.259,3.182c-4.073,2.634-7.494,2.087-10.175-2.058
|
||||
c-1.236-1.91-1.645-3.767-1.383-5.496c0.281-1.818,1.561-3.515,3.669-4.878c1.496-0.968,3.028-1.524,4.047-1.647l1.772,2.739
|
||||
l-0.577,0.373c-1.625-1.761-3.237-1.843-4.985-0.712c-3.172,2.052-2.31,5.558-0.561,8.261c1.317,2.037,4.234,5.083,7.694,2.845
|
||||
c2.216-1.434,2.193-3.208,1.508-5.295L72.255,33.341z"/>
|
||||
<path d="M76.081,28.218l2.029,4.503c0.467,1.038,0.524,1.059,2.182,0.453l0.273,0.607l-5.247,2.364l-0.273-0.607
|
||||
c1.552-0.841,1.574-0.897,1.106-1.935l-4.145-9.201c-0.467-1.038-0.524-1.059-2.174-0.434l-0.273-0.607l5.247-2.364l0.273,0.607
|
||||
c-1.542,0.812-1.574,0.897-1.115,1.916l1.782,3.955l6.304-2.84l-1.782-3.955c-0.467-1.038-0.524-1.059-2.173-0.434l-0.273-0.607
|
||||
l5.247-2.363l0.273,0.607c-1.542,0.812-1.583,0.877-1.115,1.915l4.145,9.201c0.467,1.038,0.544,1.05,2.183,0.454l0.273,0.607
|
||||
l-5.247,2.364l-0.273-0.607c1.552-0.841,1.574-0.897,1.106-1.935l-2.029-4.503L76.081,28.218z"/>
|
||||
<path d="M92.305,14.176c4.386-1.207,7.309,1.083,8.476,5.325c1.161,4.221-0.206,7.668-4.592,8.875
|
||||
c-4.428,1.218-7.324-1.057-8.485-5.278c-1.167-4.242,0.152-7.698,4.58-8.916L92.305,14.176z M92.469,14.932
|
||||
c-3.435,0.945-3.233,4.673-2.447,7.528c0.785,2.855,2.513,6.142,5.947,5.197c3.435-0.945,3.259-4.658,2.474-7.513
|
||||
c-0.785-2.855-2.539-6.156-5.953-5.217L92.469,14.932z"/>
|
||||
<path d="M107.471,23.143c0.175,1.32,0.481,1.669,2.78,1.364c2.107-0.279,3.115-0.846,3.222-3.633l0.702-0.093l0.048,3.958
|
||||
l-10.345,1.372l-0.088-0.66c1.729-0.359,1.766-0.408,1.619-1.514l-1.33-10.026c-0.149-1.128-0.198-1.165-1.957-1.04l-0.088-0.66
|
||||
l5.833-0.773l0.087,0.66c-1.859,0.355-1.896,0.403-1.746,1.531L107.471,23.143z"/>
|
||||
<path d="M119.557,22.145c0.021,1.331,0.286,1.713,2.604,1.675c2.126-0.035,3.192-0.482,3.62-3.237l0.708-0.012l-0.408,3.937
|
||||
l-10.434,0.17l-0.011-0.666c1.758-0.158,1.8-0.201,1.782-1.317l-0.166-10.112c-0.019-1.138-0.062-1.181-1.824-1.259l-0.011-0.666
|
||||
l5.882-0.096l0.011,0.666c-1.887,0.138-1.93,0.182-1.911,1.32L119.557,22.145z"/>
|
||||
<path d="M134.226,19.656l-0.129,1.346l-5.084-0.487l0.129-1.346L134.226,19.656z"/>
|
||||
<path d="M148.724,27.654c-1.346,0.118-3.414,0.119-4.817-0.194c-1.696-0.378-3.41-0.98-4.685-2.364
|
||||
c-1.217-1.349-1.766-3.626-1.191-6.203c1.042-4.672,4.219-6.889,9.143-5.792c1.969,0.439,3.342,1.185,4.191,1.814l-0.732,3.289
|
||||
l-0.671-0.149c0.106-2.549-0.987-3.761-3.187-4.251c-3.855-0.859-5.641,2.218-6.327,5.297c-0.532,2.389-0.673,6.667,3.812,7.667
|
||||
c0.648,0.145,1.217,0.162,1.762,0.085c0.432-0.058,0.734-0.232,0.883-0.903l0.621-2.786c0.215-0.964,0.134-1.092-1.803-1.677
|
||||
l0.145-0.649l5.552,1.237l-0.145,0.649c-1.332-0.143-1.435-0.078-1.653,0.907L148.724,27.654z"/>
|
||||
<path d="M152.455,29.475l0.23-0.625c1.978,0.593,2.061,0.555,2.439-0.473l1.025-2.779c0.141-0.383,0.18-0.735,0.127-1.212
|
||||
c-0.291-2.189-0.712-4.771-1.102-7.065c-0.173-0.957-0.35-1.159-1.334-1.591l0.223-0.604l5.055,1.866l-0.223,0.604l-1.172-0.295
|
||||
c-0.29-0.083-0.473,0.101-0.41,0.49c0.207,2.044,0.627,4.442,0.994,6.797l0.04,0.015c1.868-1.713,3.858-3.634,4.772-4.624
|
||||
c0.303-0.323,0.316-0.547,0.008-0.706l-1.01-0.487l0.223-0.604l4.15,1.531l-0.224,0.604c-1.299-0.388-1.472-0.291-2.476,0.506
|
||||
c-0.968,0.764-3.261,2.824-4.921,4.408c-0.343,0.309-0.507,0.568-0.679,1.031l-1.055,2.86c-0.379,1.027-0.341,1.11,1.549,1.944
|
||||
l-0.23,0.624L152.455,29.475z"/>
|
||||
<path d="M161.337,32.521l0.326-0.58c0.978,0.451,1.544,0.67,2.076-0.016c0.401-0.539,0.817-1.191,1.363-2.165l3.828-6.815
|
||||
c0.557-0.992,0.54-1.051-0.942-2.006l0.325-0.581l3.67,2.061c-0.388,3.673-0.722,7.82-1.085,11.581l0.038,0.021
|
||||
c2.481-1.832,7.1-5.321,9.285-6.976l3.595,2.019l-0.315,0.562c-1.569-0.758-1.646-0.752-2.203,0.24l-4.963,8.837
|
||||
c-0.547,0.974-0.512,1.043,0.941,2.007l-0.325,0.58l-5.018-2.817l0.325-0.581c1.617,0.76,1.676,0.744,2.223-0.229l5.004-8.912
|
||||
l-0.056-0.031c-2.714,2.071-8.53,6.464-10.447,7.948l-0.524-0.294c0.322-2.898,1.059-9.823,1.386-13.037l-0.019-0.011l-3.879,6.909
|
||||
c-0.547,0.974-0.889,1.668-1.139,2.291c-0.319,0.831,0.184,1.163,1.255,1.887l-0.326,0.58L161.337,32.521z"/>
|
||||
<path d="M186.312,49.046c0.359-4.949,0.771-9.827,1.115-14.789l-0.018-0.014l-5.003,6.115c-0.707,0.864-1.163,1.49-1.519,2.059
|
||||
c-0.457,0.763-0.02,1.177,0.926,2.089l-0.421,0.515l-3.888-3.182l0.421-0.515c0.886,0.614,1.372,0.9,2.015,0.317
|
||||
c0.488-0.46,1.012-1.032,1.719-1.896l4.948-6.049c0.721-0.881,0.731-0.927-0.58-2.139l0.421-0.515l3.092,2.529
|
||||
c-0.309,3.937-0.706,8.966-1,13.496l-0.012,0.047l4.365-5.334c0.693-0.848,1.148-1.473,1.504-2.042
|
||||
c0.472-0.779,0-1.221-0.959-2.116l0.422-0.515l3.888,3.181l-0.421,0.515c-0.869-0.6-1.325-0.89-1.982-0.29
|
||||
c-0.488,0.461-1.011,1.032-1.704,1.879l-6.199,7.577L186.312,49.046z"/>
|
||||
<path d="M193.941,56.743l0.488-0.453l0.691,0.618c0.484,0.428,0.715,0.361,0.994-0.191c0.49-0.923,1.52-2.875,1.833-3.546
|
||||
l-3.067-3.304c-1.344,0.633-2.486,1.107-3.616,1.688c-0.479,0.24-0.608,0.418-0.231,0.918l0.637,0.844l-0.488,0.453l-3.037-3.272
|
||||
l0.488-0.453c0.908,0.885,1.213,0.866,2.293,0.42c3.777-1.515,7.947-3.365,11.76-5.03l1.11,1.196
|
||||
c-1.681,3.112-4.604,8.638-6.046,11.47c-0.523,0.983-0.424,1.184,0.301,2.092l-0.487,0.453L193.941,56.743z M201.32,47.023
|
||||
l-0.043-0.047c-1.684,0.742-3.737,1.653-5.624,2.555l2.716,2.926C199.118,51.118,200.667,48.275,201.32,47.023z"/>
|
||||
<path d="M210.794,61.234c1.469-1.485,1.755-2.884,0.749-4.276c-0.98-1.356-2.396-1.446-3.597-0.578
|
||||
c-1.305,0.943-0.97,2.688-0.62,4.05c0.673,2.506,1.072,4.416-1.067,5.962c-1.739,1.257-4.174,1.296-6.299-1.644
|
||||
c-0.83-1.148-1.516-2.719-1.365-3.939l2.536-2.098l0.402,0.557c-1.56,1.763-1.995,3.614-0.901,5.127
|
||||
c1.043,1.444,2.467,1.765,3.876,0.747c1.356-0.98,1.077-2.87,0.657-4.367c-0.594-2.14-0.916-4.052,1.119-5.523
|
||||
c2.244-1.622,4.502-0.659,5.986,1.394c0.805,1.113,1.32,2.488,1.391,3.391l-2.453,1.773L210.794,61.234z"/>
|
||||
<path d="M207.416,71.994c-0.959,0.569-0.963,0.647-0.188,2.208l-0.572,0.34l-2.925-4.93l0.573-0.34
|
||||
c1.008,1.448,1.067,1.463,2.027,0.894l8.696-5.16c0.979-0.581,0.993-0.64,0.188-2.208l0.572-0.34l2.925,4.93l-0.572,0.34
|
||||
c-0.979-1.441-1.049-1.475-2.027-0.894L207.416,71.994z"/>
|
||||
<path d="M226,82.605l-0.587,0.266c-0.834-1.531-0.9-1.572-1.937-1.104l-6.357,2.876c-1.096,0.496-2.162,0.743-2.949,0.722
|
||||
c-2.188-0.047-3.505-1.808-4.345-3.667c-0.832-1.839-1.241-3.94,0.131-5.597c0.504-0.605,1.373-1.234,2.469-1.73l6.396-2.894
|
||||
c1.037-0.469,1.059-0.526,0.431-2.174l0.606-0.274l2.371,5.243l-0.605,0.274c-0.814-1.54-0.881-1.581-1.918-1.112l-6.024,2.726
|
||||
c-2.21,1-4.367,2.588-3.163,5.249c1.256,2.777,3.736,2.269,6.084,1.207l6.024-2.726c1.036-0.469,1.058-0.526,0.431-2.174
|
||||
l0.605-0.274L226,82.605z"/>
|
||||
<path d="M213.113,87.691l0.643-0.172c0.359,1.015,0.598,1.573,1.461,1.476c0.664-0.089,1.422-0.247,2.5-0.535l7.549-2.016
|
||||
c1.099-0.294,1.129-0.346,0.779-2.074l0.643-0.172l1.086,4.064c-2.9,2.285-6.102,4.94-9.047,7.304l0.012,0.042
|
||||
c3.043,0.498,8.764,1.369,11.472,1.779l1.063,3.981l-0.622,0.166c-0.552-1.652-0.61-1.703-1.71-1.409l-9.787,2.614
|
||||
c-1.078,0.288-1.104,0.361-0.779,2.074l-0.643,0.172l-1.484-5.558l0.643-0.172c0.584,1.688,0.637,1.718,1.715,1.431l9.871-2.637
|
||||
l-0.017-0.062c-3.376-0.498-10.579-1.595-12.979-1.932l-0.154-0.581c2.298-1.792,7.769-6.096,10.298-8.105l-0.006-0.021
|
||||
l-7.651,2.044c-1.078,0.288-1.814,0.529-2.435,0.784c-0.817,0.351-0.704,0.943-0.475,2.215l-0.644,0.172L213.113,87.691z"/>
|
||||
<path d="M218.8,121.336c3.931-3.029,7.844-5.972,11.773-9.022v-0.021l-7.896,0.342c-1.115,0.049-1.886,0.125-2.548,0.24
|
||||
c-0.873,0.167-0.891,0.77-0.941,2.083l-0.664,0.029l-0.219-5.021l0.666-0.029c0.132,1.069,0.242,1.623,1.105,1.714
|
||||
c0.669,0.057,1.443,0.066,2.559,0.018l7.81-0.339c1.138-0.049,1.179-0.072,1.209-1.858l0.665-0.029l0.174,3.991
|
||||
c-3.143,2.393-7.158,5.447-10.734,8.247l-0.041,0.023l6.887-0.299c1.094-0.048,1.864-0.124,2.525-0.238
|
||||
c0.896-0.168,0.91-0.814,0.939-2.126l0.665-0.029l0.218,5.021l-0.665,0.029c-0.132-1.049-0.219-1.582-1.104-1.672
|
||||
c-0.668-0.057-1.442-0.066-2.537-0.02l-9.783,0.425L218.8,121.336z"/>
|
||||
<path d="M232.498,132.985c-0.465,4.525-3.659,6.419-8.035,5.968c-4.355-0.447-7.072-2.971-6.606-7.496
|
||||
c0.47-4.568,3.64-6.443,7.995-5.994c4.376,0.449,7.119,2.933,6.648,7.501L232.498,132.985z M231.734,132.863
|
||||
c0.365-3.543-3.184-4.707-6.129-5.01c-2.945-0.304-6.635,0.115-7,3.658c-0.364,3.544,3.16,4.727,6.105,5.029
|
||||
c2.946,0.304,6.658-0.134,7.021-3.656L231.734,132.863z"/>
|
||||
<path d="M225.608,149.302c2.085,0.139,3.329-0.561,3.724-2.23c0.385-1.63-0.474-2.759-1.915-3.099
|
||||
c-1.567-0.37-2.667,1.024-3.47,2.181c-1.455,2.148-2.637,3.699-5.206,3.094c-2.089-0.493-3.712-2.309-2.878-5.84
|
||||
c0.324-1.379,1.064-2.924,2.086-3.609l3.245,0.545l-0.157,0.669c-2.354-0.026-4.039,0.855-4.468,2.673
|
||||
c-0.409,1.734,0.279,3.021,1.972,3.42c1.629,0.385,2.875-1.063,3.732-2.361c1.23-1.849,2.465-3.344,4.908-2.767
|
||||
c2.695,0.636,3.445,2.974,2.863,5.438c-0.315,1.337-1.017,2.627-1.654,3.271l-2.945-0.694L225.608,149.302z"/>
|
||||
<path d="M221.925,160.54c2.057,0.368,3.371-0.188,3.947-1.806c0.562-1.578-0.166-2.794-1.562-3.291
|
||||
c-1.517-0.54-2.765,0.725-3.688,1.785c-1.684,1.975-3.029,3.386-5.516,2.5c-2.022-0.721-3.435-2.704-2.217-6.121
|
||||
c0.475-1.334,1.381-2.789,2.471-3.357l3.166,0.9l-0.23,0.646c-2.336-0.285-4.108,0.404-4.735,2.164
|
||||
c-0.598,1.678-0.056,3.033,1.583,3.617c1.576,0.561,2.975-0.74,3.97-1.936c1.427-1.702,2.818-3.052,5.185-2.209
|
||||
c2.607,0.93,3.095,3.336,2.244,5.721c-0.461,1.295-1.3,2.499-2.004,3.068l-2.851-1.016L221.925,160.54z"/>
|
||||
<path d="M221.169,166.405c1.02,0.505,1.076,0.485,1.953-1.046l0.598,0.296l-4.562,9.217l-3.001-1.485l0.305-0.616
|
||||
c1.999,0.654,2.68,0.488,3.709-1.59l0.818-1.655c0.285-0.576,0.246-0.691-0.33-0.977l-4.35-2.152l-1.17,2.367
|
||||
c-0.753,1.52-0.301,1.863,0.759,2.579l-0.305,0.615l-4.079-2.019l0.305-0.615c1.23,0.417,1.817,0.588,2.569-0.933l1.171-2.366
|
||||
l-3.944-1.952c-1.231-0.609-1.634-0.569-2.673,1.528c-1.123,2.27-1.061,3.307,0.923,4.815l-0.313,0.635l-3.139-2.032l4.741-9.582
|
||||
l0.597,0.295c-0.685,1.626-0.646,1.693,0.354,2.188L221.169,166.405z"/>
|
||||
<path d="M199.552,179.457c4.962-0.068,9.858-0.078,14.832-0.164l0.013-0.019l-6.524-4.458c-0.922-0.631-1.585-1.031-2.184-1.336
|
||||
c-0.799-0.391-1.174,0.082-2.002,1.104l-0.549-0.376l2.834-4.149l0.551,0.376c-0.535,0.935-0.779,1.444-0.143,2.036
|
||||
c0.501,0.445,1.115,0.918,2.037,1.547l6.454,4.41c0.939,0.643,0.987,0.649,2.081-0.762l0.55,0.375l-2.253,3.299
|
||||
c-3.949,0.031-8.996,0.069-13.535,0.166l-0.048-0.006l5.692,3.889c0.904,0.618,1.566,1.019,2.164,1.323
|
||||
c0.817,0.403,1.217-0.104,2.026-1.138l0.55,0.375l-2.835,4.148l-0.55-0.375c0.523-0.917,0.772-1.396,0.118-2
|
||||
c-0.501-0.446-1.115-0.918-2.02-1.536l-8.085-5.524L199.552,179.457z"/>
|
||||
</g>
|
||||
<path d="M38.245,194.324c7.54,8.644,16.397,16.108,26.262,22.077h5.484v-7.439c1.408-2.484,3.206-4.202,5.675-5.604
|
||||
c2.665-1.51,5.286-2.171,8.341-2.171c3.056,0,5.676,0.661,8.341,2.171c2.469,1.401,4.268,3.119,5.675,5.604v8.439h9.516v-8.439
|
||||
c1.437-2.534,3.271-4.285,5.789-5.715c2.719-1.542,5.391-2.215,8.508-2.215c3.118,0,5.788,0.673,8.51,2.215
|
||||
c2.516,1.43,4.352,3.181,5.787,5.715v8.439h9.518v-8.439c1.406-2.484,3.205-4.202,5.674-5.604c2.666-1.51,5.285-2.171,8.34-2.171
|
||||
s5.676,0.661,8.342,2.171c2.469,1.401,4.268,3.119,5.676,5.604v7.439h5.484c9.865-5.969,18.721-13.434,26.262-22.077
|
||||
C141.779,190.746,101.893,190.748,38.245,194.324z"/>
|
||||
<g>
|
||||
<polygon points="68.804,76.647 70.556,76.407 70.556,72.913 68.804,73.17 "/>
|
||||
<polygon points="97.509,72.696 99.302,72.45 99.302,68.698 97.509,68.961 "/>
|
||||
<polygon points="66.255,76.997 68.009,76.756 68.009,73.292 66.255,73.545 "/>
|
||||
<polygon points="128.406,68.44 130.277,68.186 130.277,64.151 128.406,64.424 "/>
|
||||
<polygon points="125.578,68.829 127.609,68.551 127.609,64.54 125.578,64.836 "/>
|
||||
<polygon points="94.964,73.045 96.712,72.803 96.712,69.075 94.964,69.333 "/>
|
||||
<polygon points="151.562,78.686 154.484,78.87 154.484,74.477 151.562,74.196 "/>
|
||||
<polygon points="145.195,78.29 148.433,78.491 148.433,73.895 145.195,73.586 "/>
|
||||
<polygon points="139.25,77.918 142.222,78.104 142.222,73.301 139.25,73.016 "/>
|
||||
<polygon points="145.195,88.7 148.433,88.858 148.433,83.532 145.195,83.354 "/>
|
||||
<polygon points="151.562,89.012 154.484,89.153 154.484,83.864 151.562,83.704 "/>
|
||||
<polygon points="139.25,88.411 142.222,88.553 142.222,83.194 139.25,83.034 "/>
|
||||
<polygon points="156.076,89.229 158.837,89.368 158.837,84.098 156.076,83.95 "/>
|
||||
<polygon points="144.847,68.024 147.05,68.182 147.05,64.711 144.847,64.522 "/>
|
||||
<polygon points="150.394,64.997 148.193,64.809 148.193,68.264 150.394,68.422 "/>
|
||||
<polygon points="128.943,78.528 130.939,78.262 130.939,73.491 128.943,73.778 "/>
|
||||
<polygon points="128.943,89.077 130.939,88.834 130.939,83.502 128.943,83.737 "/>
|
||||
<polygon points="125.341,79.006 127.482,78.719 127.482,73.983 125.341,74.288 "/>
|
||||
<polygon points="125.341,89.516 127.482,89.254 127.482,83.907 125.341,84.159 "/>
|
||||
<polygon points="97.749,82.672 100.169,82.35 100.169,77.856 97.749,78.2 "/>
|
||||
<polygon points="97.749,92.877 100.169,92.586 100.169,87.116 97.749,87.399 "/>
|
||||
<polygon points="93.888,83.182 96.116,82.889 96.116,78.43 93.888,78.749 "/>
|
||||
<polygon points="93.888,93.35 96.116,93.079 96.116,87.592 93.888,87.856 "/>
|
||||
<polygon points="65.62,86.938 67.331,86.709 67.331,82.512 65.62,82.758 "/>
|
||||
<polygon points="65.62,96.795 67.331,96.586 67.331,90.971 65.62,91.174 "/>
|
||||
<polygon points="65.62,106.766 67.331,106.604 67.331,101.407 65.62,101.588 "/>
|
||||
<path d="M210.435,126.26c-0.646-1.156-0.623-3.271-1.867-5.139c-1.246-1.871-2.648-3.195-3.816-4.363
|
||||
c-0.477-0.477-1.406-0.953-2.479-1.416c0.006-0.055,0.018-0.109,0.018-0.166c0-0.52-0.213-0.996-0.549-1.354
|
||||
c0.088-0.123,0.184-0.244,0.248-0.371c0.057-0.109,0.102-0.221,0.145-0.332c0.842-0.158,1.607-0.66,2.061-1.43
|
||||
c0.26-0.445,0.408-0.986,0.408-1.5c-0.061-0.453-0.094-0.697-0.1-0.746c-0.008-0.047-0.012-0.094-0.02-0.141
|
||||
c-0.012-0.059-0.023-0.111-0.033-0.156c-0.059-0.256-0.186-0.5-0.311-0.73c-0.148-0.277-0.373-0.531-0.605-0.746
|
||||
c-0.449-0.416-1.076-0.754-1.711-0.789c-0.033-0.023-0.061-0.053-0.096-0.074c0.324-0.6,0.418-1.311,0.238-1.984
|
||||
c-0.01-0.035-0.02-0.07-0.029-0.105c-0.289-1.082-1.256-1.824-2.322-1.936c-0.086-0.914-0.756-1.785-1.65-2.031
|
||||
c-0.834-0.229-1.684,0.069-2.238,0.651V89.368h0.641l-12.473-20.229l-9.611-0.742l-9.184-1.537l-2.539,4.676l-1.814-1.389
|
||||
l-5.678-12.686l-6.162-0.33l-0.898-1.898l-8.564,1.393l-0.248-0.012l-9.08-1.307c-0.416-0.479-0.744-0.791-0.744-0.791v-1.488
|
||||
l0.291-0.133v-0.424l-0.475-0.291v-3.77l0.449-0.32v-0.371c-1.684-1.02-2.492-4.047-2.629-5.068
|
||||
c-0.227-1.701-0.221-6.355-0.213-7.664c0.221-0.143,0.367-0.389,0.367-0.67c0-0.309-0.178-0.572-0.432-0.705l-0.332-5.027v-0.059
|
||||
h-0.066v0.059l-0.33,5.027c-0.258,0.133-0.432,0.396-0.432,0.705c0,0.281,0.146,0.527,0.367,0.67
|
||||
c0.006,1.309,0.012,5.963-0.215,7.664c-0.139,1.021-0.945,4.049-2.629,5.068v0.371l0.451,0.32v3.77l-0.477,0.291v0.424l0.291,0.133
|
||||
v1.488c0,0-0.559,0.531-1.143,1.271c-0.582,0.744-1.193,2.232-1.299,2.789c-0.049,0.264-0.15,0.699-0.238,1.08l-11.541,1.898
|
||||
l-12.623-1.758l-1.32,4.027l-14.391,2.342l-13.076-1.879l-4.49,12.324l-2.062,3.758l0.303,0.023v37.375l0.865-0.395h0.039V80.323
|
||||
l9.051-1.1l-0.902,1.674v1.162l-1.445,0.207v4.211l1.445-0.191v4.312l-1.445,0.166v5.607l1.445-0.178v4.875l-1.445,0.15v5.215
|
||||
l1.445-0.137v9.175l0.902-0.479V81.083l3.043-0.424v32.723l2.08-1.103V87.717l4.871-0.584v18.393l0.902-0.533V79.565l4.412-0.615
|
||||
v23.435l2.076-1.228V86.25l2.654-0.32v13.657l0.902-0.534V76.178l10.963-1.467v19.285l0.9-0.399V76.512l5.355-0.748v13.475
|
||||
l2.076-1.457v-4.274l5.123-0.613v3.9l0.904,0.033v-12.19l3.682-0.51v15.345l2.079,0.096v-15.31l0.662-0.023l-1.803-2.072
|
||||
l11.952-1.602v16.868l0.904,0.033V70.536l27.285,1.805v24.243l0.902,0.252V71.852l1.98-3.535l11.133,18.951v13.241l0.9,0.252
|
||||
V88.055l14.902,1.67v18.256c-0.4-0.449-0.868-0.799-1.359-0.975c-0.258-0.145-0.547-0.252-0.855-0.326
|
||||
c-0.008-0.004-0.018-0.021-0.025-0.029c0.244-0.404,0.334-0.896,0.17-1.404c-0.434-1.334-1.467-2.334-2.566-2.438
|
||||
c0-0.006,0-0.01-0.004-0.016c-0.252-1.629-2.125-1.715-3.045-0.754c-0.332,0.346-0.529,0.889-0.637,1.531
|
||||
c-1.314-0.43-2.732,0.146-3.504,1.33c-0.031,0.043-0.053,0.092-0.08,0.137c-0.184,0.207-0.338,0.424-0.441,0.648
|
||||
c-0.042-0.061-0.096-0.111-0.142-0.169l-3.458,1.938v16.888c1.988-2.09,3.71-3.649,7.139-1.911
|
||||
c4.764,2.414,7.508-0.662,10.482,0.35c0.59,0.199,2.551,2.1,3.916,2.25c8.988,0.98,5.174,1.85,8.99,4.965
|
||||
c3.816,3.111,6.461,3.736,6.461,3.736S211.138,127.504,210.435,126.26z M62.874,79.252l1.391-2.348l4.021-11.07l5.414,10.99
|
||||
l-0.967-0.029l-0.746,1.264L62.874,79.252z M71.53,80.571l1.443-2.758l1.506,2.262L71.53,80.571z M82.317,78.915l2.236-5.172
|
||||
l2.035,4.537L82.317,78.915z M91.915,75.143l1.436-2.656l3.557-11.051l4.215,9.793l1.719,2.354L91.915,75.143z M104.2,75.862
|
||||
l2.547-5.131l2.754,4.387L104.2,75.862z M117.702,73.973l1.664-2.564l1.791,2.086L117.702,73.973z M121.563,70.903l2.145-3.334
|
||||
l4.121-10.93l4.041,9.773l1.818,2.758L121.563,70.903z M140.953,69.754l6.711-13.01l6.752,14.117L140.953,69.754z M194.824,112.858
|
||||
c-0.49-0.027-1.012-0.048-1.617-0.071c0.246-0.854,0.312-1.764,0.15-2.637c-0.04-0.218-0.104-0.431-0.178-0.642V89.368h1.645
|
||||
V112.858z M195.726,106.941v-0.02c0.002,0.002,0.002,0.005,0.004,0.007C195.728,106.932,195.728,106.937,195.726,106.941z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon points="140.953,114.856 141.994,113.17 128.777,112.874 129.818,114.655 "/>
|
||||
<polygon points="125.207,114.569 126.285,112.817 115.017,112.565 116.091,114.405 "/>
|
||||
<polygon points="111.911,114.329 113.026,112.52 101.927,112.272 102.791,114.16 "/>
|
||||
<path d="M62.276,143.643c-1.496-0.008-2.523,0.865-2.523,0.865v8.215h5.037v-8.16C64.79,144.563,63.767,143.649,62.276,143.643z"/>
|
||||
<path d="M55.677,143.581c-1.506-0.006-2.539,0.871-2.539,0.871v8.271h5.07v-8.215C58.208,144.508,57.177,143.588,55.677,143.581z"
|
||||
/>
|
||||
<path d="M48.61,143.555c-1.547-0.006-2.605,0.873-2.605,0.873v8.295h5.203v-8.238C51.208,144.485,50.149,143.563,48.61,143.555z"/>
|
||||
<polygon points="154.39,115.1 155.396,113.471 144.13,113.219 145.132,114.932 "/>
|
||||
<polygon points="167.691,115.342 168.257,113.76 157.148,113.512 158.117,115.168 "/>
|
||||
<path d="M174.585,104.549l0.547-0.904l-35.502-9.943l-2.787-0.129v-2.666l-3.088-0.113l-2.59,1.018v1.499l-13.702-0.634v-2.473
|
||||
l-3.191-0.117l-1.893,1.328v2.275l-17.107,7.581l-12.25,7.242l0.684,0.869l0.791,0.283v2.231l-16.714,8.86h-4.704l-7.66,3.494
|
||||
l-5.266,3.338l0.51,0.709l0.754,0.131l0.02-0.011v9.767l-17.283-0.227l1.085,3.008l33.721,0.465v18.12l-2.119-1.948H41.656
|
||||
l1.039,1.375H66.05l1.395,1.432h1.516h0.934h0.037v-2.942l14.566-0.41v19.718l-19.328-3.352v2.537l31.276,6.672h32.619v-3.955
|
||||
l-31.262,0.13v-22.149l73.336,0.638v21.382h-30.17v3.955h32.318v-3.955h-0.34v-21.701l0.693-1.074v-0.371l-0.693-0.008v-19.729
|
||||
l0.693-1.229v-0.369l-0.693-0.008v-28.494L174.585,104.549z M55.655,138.24l-3.465-0.046v-8.197l3.465-1.702V138.24z
|
||||
M55.655,127.519l-3.465,1.796v-1.329l3.465-1.983V127.519z M84.497,155.341l-14.566,0.671v-14.079l14.566-3.58V155.341z
|
||||
M84.497,136.637l-14.622,3.856l-2.544-2.101l-10.773-0.142v-7.404l27.939-13.711V136.637z M84.497,116.137l-27.939,13.956v-1.182
|
||||
l27.939-14.68V116.137z M95.995,178.767l-9.746-1.689v-20.071l9.746-0.274V178.767z M95.995,154.812l-9.746,0.449v-17.339
|
||||
l9.746-2.396V154.812z M95.995,133.605l-9.746,2.57v-23.368l9.746-4.776V133.605z M95.995,106.692l-9.746,4.959v-2.018
|
||||
c2.139-1.114,8.137-4.322,9.746-5.183V106.692z M143.679,98.34l1.734,1.925l-3.203-0.048l-1.439-1.914L143.679,98.34z
|
||||
M123.72,97.954l1.494,1.9l-3.581-0.074l-1.436-1.906L123.72,97.954z M171.138,155.502l-73.336-0.814v-19.414l73.336,0.611V155.502
|
||||
z M171.138,134.167l-73.336-0.793v-25.828l73.336,1.637V134.167z M171.138,108.05l-73.336-1.732v-2.05l73.336,1.836V108.05z"/>
|
||||
<polygon points="110.007,175.602 110.007,165.472 103.979,165.407 103.979,175.548 "/>
|
||||
<polygon points="110.007,143.072 103.979,142.982 103.979,153.899 110.007,153.976 "/>
|
||||
<polygon points="103.979,130.048 110.458,130.159 110.458,116.338 103.979,116.211 "/>
|
||||
<polygon points="117.163,130.274 123.937,130.389 123.937,116.602 117.163,116.469 "/>
|
||||
<polygon points="146.687,130.778 153.091,130.887 153.091,117.172 146.687,117.047 "/>
|
||||
<polygon points="159.794,131.001 165.739,131.103 165.821,117.421 159.794,117.303 "/>
|
||||
<polygon points="131.145,130.512 139.304,130.652 139.304,116.902 131.102,116.742 "/>
|
||||
<polygon points="123.033,143.264 117.163,143.177 117.163,154.068 123.033,154.143 "/>
|
||||
<polygon points="139.757,143.511 130.416,143.373 130.416,154.238 139.757,154.358 "/>
|
||||
<polygon points="153.091,143.708 146.687,143.613 146.687,154.447 153.091,154.529 "/>
|
||||
<polygon points="165.65,143.893 159.794,143.807 159.794,154.615 165.592,154.689 "/>
|
||||
<polygon points="123.033,175.719 123.033,165.612 117.163,165.549 117.163,175.667 "/>
|
||||
<polygon points="146.687,165.867 146.687,175.93 153.091,175.988 153.091,165.936 "/>
|
||||
<polygon points="159.794,166.007 159.794,176.048 165.453,176.098 165.514,166.07 "/>
|
||||
<polygon points="130.416,182.178 139.757,182.178 139.757,165.792 130.416,165.692 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,249 @@
|
||||
<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/dark.css">
|
||||
|
||||
<div class="eventlist-layout">
|
||||
<div class="eventlist-sidebar">
|
||||
<form method="get" id="event-filter-form">
|
||||
<h3>Filtern</h3>
|
||||
<input type="hidden" name="archive_filter_applied" value="1">
|
||||
|
||||
{% if availableArchives is not empty %}
|
||||
<fieldset>
|
||||
<legend>Event-Archive</legend>
|
||||
|
||||
{% for archive in availableArchives %}
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="archive_ids[]"
|
||||
value="{{ archive.id }}"
|
||||
class="archive-filter-checkbox"
|
||||
{% if archive.id in selectedArchiveIds %}checked{% endif %}
|
||||
>
|
||||
{{ archive.title }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
<label for="filter-from-input">
|
||||
<span>Von</span>
|
||||
</label>
|
||||
<div class="flatpickr" id="filter-from-wrap">
|
||||
<input id="filter-from-input" type="text" name="from" value="{{ from }}" autocomplete="off" data-input>
|
||||
<a id="filter-from-clear" class="input-button" title="clear" data-clear {% if from is empty %}hidden{% endif %}>×</a>
|
||||
</div>
|
||||
|
||||
<label for="filter-to-input">
|
||||
<span>Bis</span>
|
||||
</label>
|
||||
<div class="flatpickr" id="filter-to-wrap">
|
||||
<input id="filter-to-input" type="text" name="to" value="{{ to }}" autocomplete="off" data-input>
|
||||
<a id="filter-to-clear" class="input-button" title="clear" data-clear {% if to is empty %}hidden{% endif %}>×</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/events/pdf" id="pdf-export-form">
|
||||
<input type="hidden" name="from" value="{{ from }}">
|
||||
<input type="hidden" name="to" value="{{ to }}">
|
||||
|
||||
{% if pdfError == 'no_selection' %}
|
||||
<p>Bitte wählen Sie mindestens einen Termin für den PDF-Export aus.</p>
|
||||
{% elseif pdfError == 'not_found' %}
|
||||
<p>Die ausgewählten Termine konnten nicht geladen werden.</p>
|
||||
{% endif %}
|
||||
|
||||
<p id="pdf-selection-error" role="alert" aria-live="polite" hidden>Bitte wählen Sie mindestens einen Termin für den PDF-Export aus.</p>
|
||||
|
||||
<p>
|
||||
<label for="pdf_heading">Überschrift (optional)</label>
|
||||
<input id="pdf_heading" type="text" name="pdf_heading" value="">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="pdf_intro">Einleitungstext (optional)</label>
|
||||
<textarea id="pdf_intro" name="pdf_intro" rows="4"></textarea>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button type="submit">PDF erstellen</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="eventlist-results">
|
||||
{% if events is not empty %}
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="select-all-events">
|
||||
Alle auswählen
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
{% for event in events %}
|
||||
<li class="pid-{{ event.pid }}{% if event.isFeatured %} is-featured{% endif %}">
|
||||
<label>
|
||||
<input type="checkbox" name="event_ids[]" value="{{ event.id }}" class="event-select-checkbox" form="pdf-export-form">
|
||||
{{ event.title }}
|
||||
</label>
|
||||
|
||||
<p>
|
||||
{{ event.startDate }}{% if event.endDate %} - {{ event.endDate }}{% endif %}
|
||||
</p>
|
||||
|
||||
{% if event.url %}
|
||||
<a href="{{ event.url }}">weiterlesen…</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Keine Termine gefunden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/de.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const filterForm = document.getElementById('event-filter-form');
|
||||
const filterFromWrap = document.getElementById('filter-from-wrap');
|
||||
const filterToWrap = document.getElementById('filter-to-wrap');
|
||||
const filterFromInput = document.getElementById('filter-from-input');
|
||||
const filterToInput = document.getElementById('filter-to-input');
|
||||
const filterFromClear = document.getElementById('filter-from-clear');
|
||||
const filterToClear = document.getElementById('filter-to-clear');
|
||||
const archiveFilterCheckboxes = document.querySelectorAll('.archive-filter-checkbox');
|
||||
|
||||
if (filterForm && filterFromInput && filterToInput) {
|
||||
const submitFilterForm = function () {
|
||||
filterForm.submit();
|
||||
};
|
||||
|
||||
const updateClearButtons = function () {
|
||||
if (filterFromClear) {
|
||||
filterFromClear.hidden = filterFromInput.value === '';
|
||||
}
|
||||
|
||||
if (filterToClear) {
|
||||
filterToClear.hidden = filterToInput.value === '';
|
||||
}
|
||||
};
|
||||
|
||||
updateClearButtons();
|
||||
|
||||
if (typeof flatpickr === 'function') {
|
||||
const commonConfig = {
|
||||
wrap: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
allowInput: true,
|
||||
locale: (window.flatpickr && window.flatpickr.l10ns && window.flatpickr.l10ns.de) ? window.flatpickr.l10ns.de : 'default',
|
||||
onChange: [
|
||||
function () {
|
||||
updateClearButtons();
|
||||
submitFilterForm();
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (filterFromWrap) {
|
||||
flatpickr(filterFromWrap, commonConfig);
|
||||
}
|
||||
|
||||
if (filterToWrap) {
|
||||
flatpickr(filterToWrap, commonConfig);
|
||||
}
|
||||
|
||||
filterFromInput.addEventListener('input', function () {
|
||||
updateClearButtons();
|
||||
|
||||
if (filterFromInput.value === '') {
|
||||
submitFilterForm();
|
||||
}
|
||||
});
|
||||
|
||||
filterToInput.addEventListener('input', function () {
|
||||
updateClearButtons();
|
||||
|
||||
if (filterToInput.value === '') {
|
||||
submitFilterForm();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
filterFromInput.addEventListener('change', function () {
|
||||
updateClearButtons();
|
||||
submitFilterForm();
|
||||
});
|
||||
|
||||
filterToInput.addEventListener('change', function () {
|
||||
updateClearButtons();
|
||||
submitFilterForm();
|
||||
});
|
||||
}
|
||||
|
||||
if (filterFromClear) {
|
||||
filterFromClear.addEventListener('click', function () {
|
||||
window.setTimeout(updateClearButtons, 0);
|
||||
});
|
||||
}
|
||||
|
||||
if (filterToClear) {
|
||||
filterToClear.addEventListener('click', function () {
|
||||
window.setTimeout(updateClearButtons, 0);
|
||||
});
|
||||
}
|
||||
|
||||
archiveFilterCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.addEventListener('change', submitFilterForm);
|
||||
});
|
||||
}
|
||||
|
||||
const selectAll = document.getElementById('select-all-events');
|
||||
|
||||
if (!selectAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventCheckboxes = document.querySelectorAll('.event-select-checkbox');
|
||||
const exportForm = document.getElementById('pdf-export-form');
|
||||
const selectionError = document.getElementById('pdf-selection-error');
|
||||
|
||||
selectAll.addEventListener('change', function () {
|
||||
eventCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.checked = selectAll.checked;
|
||||
});
|
||||
});
|
||||
|
||||
eventCheckboxes.forEach(function (checkbox) {
|
||||
checkbox.addEventListener('change', function () {
|
||||
const allChecked = Array.from(eventCheckboxes).every(function (item) {
|
||||
return item.checked;
|
||||
});
|
||||
|
||||
selectAll.checked = allChecked;
|
||||
|
||||
if (selectionError && checkbox.checked) {
|
||||
selectionError.hidden = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (exportForm) {
|
||||
exportForm.addEventListener('submit', function (event) {
|
||||
const anyChecked = Array.from(eventCheckboxes).some(function (item) {
|
||||
return item.checked;
|
||||
});
|
||||
|
||||
if (!anyChecked) {
|
||||
event.preventDefault();
|
||||
|
||||
if (selectionError) {
|
||||
selectionError.hidden = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: DejaVu Sans, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.4;
|
||||
margin: 1.3cm 1.2cm 1.2cm 1.2cm;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.55cm 0;
|
||||
}
|
||||
|
||||
.pdf-title {
|
||||
max-width: 70%;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 0.38cm 0;
|
||||
}
|
||||
|
||||
.event-list {
|
||||
margin: 0.45cm 0 0 0;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
margin: 0 0 0.18cm 0;
|
||||
}
|
||||
|
||||
.pdf-logo {
|
||||
position: absolute;
|
||||
top: 0.3cm;
|
||||
right: 1.3cm;
|
||||
width: 2.4cm;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if logoDataUri %}
|
||||
<img src="{{ logoDataUri }}" alt="Gymnasium Nossen" class="pdf-logo">
|
||||
{% endif %}
|
||||
|
||||
<h1 class="pdf-title">{{ heading }}</h1>
|
||||
|
||||
{% if introText %}
|
||||
<p>{{ introText }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if dateRange %}
|
||||
<p>{{ dateRange }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="event-list">
|
||||
{% for event in events %}
|
||||
<p class="event-item">
|
||||
{{ event.title }} –
|
||||
<span class="event-time">
|
||||
{{ event.startText }}
|
||||
{% if event.endText %}
|
||||
- {{ event.endText }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "gymnasium-nossen/gymnasium-nossen-bundle",
|
||||
"description": "Contao frontend module with date-range filtered upcoming events for members.",
|
||||
"type": "contao-bundle",
|
||||
"license": "proprietary",
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"contao/core-bundle": "^5.7",
|
||||
"contao/calendar-bundle": "^5.7",
|
||||
"dompdf/dompdf": "^3.1",
|
||||
"contao/manager-plugin": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GymnasiumNossenBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"contao-manager-plugin": "GymnasiumNossenBundle\\Contao\\Manager\\Plugin"
|
||||
},
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$GLOBALS['FE_MOD']['events']['gymnasium_eventlist'] = GymnasiumNossenBundle\Controller\EventListModuleController::class;
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$GLOBALS['TL_DCA']['tl_module']['palettes']['gymnasium_eventlist'] = '{title_legend},name,headline,type;{config_legend},cal_calendar;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$GLOBALS['TL_LANG']['FMD']['gymnasium_eventlist'] = ['gefilterte Eventliste', 'Zeigt zukünftige Termine mit optionalem Datumsbereichs-Filter für Mitglieder.'];
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$GLOBALS['TL_LANG']['FMD']['gymnasium_eventlist'] = ['Filtered event list', 'Shows upcoming events with an optional date range filter for members.'];
|
||||
@@ -0,0 +1,37 @@
|
||||
<form method="get">
|
||||
<label>
|
||||
<span>Von</span>
|
||||
<input type="date" name="from" value="{{ from }}">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Bis</span>
|
||||
<input type="date" name="to" value="{{ to }}">
|
||||
</label>
|
||||
|
||||
<button type="submit">FILTERN</button>
|
||||
|
||||
{% if isFiltered %}
|
||||
<a href="{{ resetUrl }}">ALLE TERMINE ANZEIGEN</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if events is not empty %}
|
||||
<ul>
|
||||
{% for event in events %}
|
||||
<li>
|
||||
<h3>{{ event.title }}</h3>
|
||||
|
||||
<p>
|
||||
{{ event.startDate }}{% if event.endDate %} - {{ event.endDate }}{% endif %}
|
||||
</p>
|
||||
|
||||
{% if event.url %}
|
||||
<a href="{{ event.url }}">weiterlesen…</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Keine Termine gefunden.</p>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GymnasiumNossenBundle\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 Contao\ManagerPlugin\Routing\RoutingPluginInterface;
|
||||
use GymnasiumNossenBundle\GymnasiumNossenBundle;
|
||||
use Symfony\Component\Config\Loader\LoaderResolverInterface;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
class Plugin implements BundlePluginInterface, RoutingPluginInterface
|
||||
{
|
||||
public function getBundles(ParserInterface $parser): iterable
|
||||
{
|
||||
return [
|
||||
BundleConfig::create(GymnasiumNossenBundle::class)
|
||||
->setLoadAfter([ContaoCoreBundle::class, ContaoCalendarBundle::class]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRouteCollection(LoaderResolverInterface $resolver, KernelInterface $kernel): RouteCollection|null
|
||||
{
|
||||
return $resolver
|
||||
->resolve(__DIR__.'/../../Resources/config/routes.yaml')
|
||||
->load(__DIR__.'/../../Resources/config/routes.yaml')
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GymnasiumNossenBundle\Controller;
|
||||
|
||||
use Contao\CalendarEventsModel;
|
||||
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
|
||||
use Contao\CoreBundle\Routing\ContentUrlGenerator;
|
||||
use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||
use Contao\CoreBundle\Twig\Interop\ContextFactory;
|
||||
use Contao\FrontendUser;
|
||||
use Contao\ModuleModel;
|
||||
use Contao\StringUtil;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Exception\ExceptionInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
class EventListModuleController extends AbstractFrontendModuleController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly ContentUrlGenerator $contentUrlGenerator,
|
||||
private readonly ContextFactory $contextFactory,
|
||||
private readonly Security $security,
|
||||
private readonly Environment $twig,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||
{
|
||||
$activeRequest = $this->requestStack->getMainRequest() ?? $request;
|
||||
|
||||
$configuredArchiveIds = $this->getConfiguredArchiveIds($model);
|
||||
$availableArchives = $this->fetchAllowedArchives($configuredArchiveIds);
|
||||
$allowedArchiveIds = array_map(static fn (array $archive): int => (int) $archive['id'], $availableArchives);
|
||||
$archiveFilterApplied = '1' === (string) $activeRequest->query->get('archive_filter_applied', '0');
|
||||
$selectedArchiveIds = $this->resolveSelectedArchiveIds($activeRequest->query->all('archive_ids'), $allowedArchiveIds, $archiveFilterApplied);
|
||||
|
||||
$fromRaw = $activeRequest->query->get('from');
|
||||
$toRaw = $activeRequest->query->get('to');
|
||||
|
||||
$from = $this->parseDateParameter(is_string($fromRaw) ? $fromRaw : null);
|
||||
$to = $this->parseDateParameter(is_string($toRaw) ? $toRaw : null);
|
||||
|
||||
$isDateRangeFilterActive = null !== $from && null !== $to && $from <= $to;
|
||||
|
||||
$events = $this->fetchEvents($selectedArchiveIds, $from, $to, $isDateRangeFilterActive);
|
||||
|
||||
$template->set('events', $events);
|
||||
$template->set('availableArchives', $availableArchives);
|
||||
$template->set('selectedArchiveIds', $selectedArchiveIds);
|
||||
$template->set('from', is_string($fromRaw) ? $fromRaw : '');
|
||||
$template->set('to', is_string($toRaw) ? $toRaw : '');
|
||||
$template->set('isFiltered', $isDateRangeFilterActive);
|
||||
$template->set('resetUrl', ($activeRequest->getBaseUrl() ?: '').$activeRequest->getPathInfo());
|
||||
$template->set('pdfError', $this->normalizePdfError($activeRequest->query->get('pdf_error')));
|
||||
|
||||
$templatePath = __DIR__.'/../../Resources/views/frontend/module_event_list.html.twig';
|
||||
$templateContent = file_get_contents($templatePath);
|
||||
|
||||
if (false === $templateContent) {
|
||||
throw new \RuntimeException('The module template file could not be read: '.$templatePath);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->twig->createTemplate($templateContent)->render($this->contextFactory->fromData($template->getData())),
|
||||
);
|
||||
}
|
||||
|
||||
private function parseDateParameter(?string $rawValue): ?DateTimeImmutable
|
||||
{
|
||||
if (null === $rawValue || '' === $rawValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $rawValue);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
|
||||
if (
|
||||
false === $date
|
||||
|| (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0))
|
||||
|| $date->format('Y-m-d') !== $rawValue
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: int, pid: int, isFeatured: bool, title: string, startDate: string, endDate: string|null, url: string|null}>
|
||||
*/
|
||||
private function fetchEvents(array $archiveIds, ?DateTimeImmutable $from, ?DateTimeImmutable $to, bool $isDateRangeFilterActive): array
|
||||
{
|
||||
if ([] === $archiveIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$effectiveStartExpression = 'COALESCE(NULLIF(e.startTime, 0), NULLIF(e.startDate, 0), 0)';
|
||||
$effectiveEndExpression = 'COALESCE(NULLIF(e.endTime, 0), NULLIF(e.endDate, 0), '.$effectiveStartExpression.')';
|
||||
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
|
||||
$queryBuilder
|
||||
->select('e.id', 'e.pid', 'e.featured', 'e.alias', 'e.source', 'e.url', 'e.jumpTo', 'e.articleId', 'e.title', 'e.addTime', 'e.startDate', 'e.endDate', 'e.startTime', 'e.endTime')
|
||||
->from('tl_calendar_events', 'e')
|
||||
->where('e.pid IN (:archiveIds)')
|
||||
->andWhere('e.published = :published')
|
||||
->setParameter('archiveIds', $archiveIds, ArrayParameterType::INTEGER)
|
||||
->setParameter('published', 1)
|
||||
->orderBy($effectiveStartExpression, 'ASC')
|
||||
;
|
||||
|
||||
if ($isDateRangeFilterActive && null !== $from && null !== $to) {
|
||||
$fromTimestamp = $from->setTime(0, 0)->getTimestamp();
|
||||
$toTimestamp = $to->setTime(23, 59, 59)->getTimestamp();
|
||||
|
||||
$queryBuilder
|
||||
->andWhere($effectiveStartExpression.' <= :toTimestamp')
|
||||
->andWhere($effectiveEndExpression.' >= :fromTimestamp')
|
||||
->setParameter('toTimestamp', $toTimestamp)
|
||||
->setParameter('fromTimestamp', $fromTimestamp)
|
||||
;
|
||||
} else {
|
||||
$todayStart = (new DateTimeImmutable('today'))->setTime(0, 0)->getTimestamp();
|
||||
|
||||
$queryBuilder
|
||||
->andWhere($effectiveStartExpression.' >= :todayStart')
|
||||
->setParameter('todayStart', $todayStart)
|
||||
;
|
||||
}
|
||||
|
||||
$rows = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$events = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
['startDateText' => $startDateText, 'endDateText' => $endDateText] = $this->formatEventDateTimeTexts($row);
|
||||
|
||||
$eventModel = new CalendarEventsModel($row);
|
||||
|
||||
try {
|
||||
$eventUrl = $this->contentUrlGenerator->generate($eventModel);
|
||||
} catch (ExceptionInterface) {
|
||||
$eventUrl = null;
|
||||
}
|
||||
|
||||
$events[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'pid' => (int) ($row['pid'] ?? 0),
|
||||
'isFeatured' => '1' === (string) ($row['featured'] ?? ''),
|
||||
'title' => $this->normalizeTitle((string) ($row['title'] ?? '')),
|
||||
'startDate' => $startDateText,
|
||||
'endDate' => $endDateText,
|
||||
'url' => $eventUrl,
|
||||
];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
private function normalizeTitle(string $title): string
|
||||
{
|
||||
$decoded = $title;
|
||||
|
||||
for ($index = 0; $index < 2; ++$index) {
|
||||
$next = html_entity_decode($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
if ($next === $decoded) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decoded = $next;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*
|
||||
* @return array{startDateText: string, endDateText: string|null}
|
||||
*/
|
||||
private function formatEventDateTimeTexts(array $row): array
|
||||
{
|
||||
$addTime = (int) ($row['addTime'] ?? 0) === 1;
|
||||
$startDateTimestamp = (int) ($row['startDate'] ?? 0);
|
||||
$endDateTimestamp = (int) ($row['endDate'] ?? 0);
|
||||
$startTimeTimestamp = (int) ($row['startTime'] ?? 0);
|
||||
$endTimeTimestamp = (int) ($row['endTime'] ?? 0);
|
||||
|
||||
if ($addTime) {
|
||||
$startTimestamp = $startTimeTimestamp > 0 ? $startTimeTimestamp : $startDateTimestamp;
|
||||
$start = (new DateTimeImmutable())->setTimestamp($startTimestamp);
|
||||
$startText = $start->format('d.m.Y H:i');
|
||||
|
||||
if ($endDateTimestamp <= 0) {
|
||||
return ['startDateText' => $startText, 'endDateText' => null];
|
||||
}
|
||||
|
||||
$endTimestamp = $endTimeTimestamp > 0 ? $endTimeTimestamp : $endDateTimestamp;
|
||||
$end = (new DateTimeImmutable())->setTimestamp($endTimestamp);
|
||||
$sameDay = $start->format('Y-m-d') === $end->format('Y-m-d');
|
||||
$endText = $sameDay ? $end->format('H:i') : $end->format('d.m.Y H:i');
|
||||
|
||||
return ['startDateText' => $startText, 'endDateText' => $endText];
|
||||
}
|
||||
|
||||
$startTimestamp = $startDateTimestamp > 0 ? $startDateTimestamp : $startTimeTimestamp;
|
||||
$start = (new DateTimeImmutable())->setTimestamp($startTimestamp);
|
||||
$startText = $start->format('d.m.Y');
|
||||
|
||||
if ($endDateTimestamp <= 0) {
|
||||
return ['startDateText' => $startText, 'endDateText' => null];
|
||||
}
|
||||
|
||||
$end = (new DateTimeImmutable())->setTimestamp($endDateTimestamp);
|
||||
$endText = $end->format('d.m.Y');
|
||||
|
||||
return ['startDateText' => $startText, 'endDateText' => $endText !== $startText ? $endText : null];
|
||||
}
|
||||
|
||||
private function normalizePdfError(mixed $rawValue): ?string
|
||||
{
|
||||
if (!is_string($rawValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($rawValue) {
|
||||
'no_selection', 'not_found' => $rawValue,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
private function getConfiguredArchiveIds(ModuleModel $model): array
|
||||
{
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $value): int => (int) $value,
|
||||
StringUtil::deserialize($model->cal_calendar, true),
|
||||
))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $configuredArchiveIds
|
||||
*
|
||||
* @return list<array{id: int, title: string}>
|
||||
*/
|
||||
private function fetchAllowedArchives(array $configuredArchiveIds): array
|
||||
{
|
||||
if ([] === $configuredArchiveIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
$memberGroupIds = [];
|
||||
|
||||
if ($user instanceof FrontendUser) {
|
||||
$memberGroupIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $value): int => (int) $value,
|
||||
StringUtil::deserialize($user->groups, true),
|
||||
))));
|
||||
}
|
||||
|
||||
$rows = $this->connection->createQueryBuilder()
|
||||
->select('c.id', 'c.title', 'c.protected', 'c.groups')
|
||||
->from('tl_calendar', 'c')
|
||||
->where('c.id IN (:ids)')
|
||||
->setParameter('ids', $configuredArchiveIds, ArrayParameterType::INTEGER)
|
||||
->orderBy('c.title', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative()
|
||||
;
|
||||
|
||||
$archives = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$isProtected = (int) ($row['protected'] ?? 0) === 1;
|
||||
|
||||
if ($isProtected) {
|
||||
if (!$user instanceof FrontendUser) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$calendarGroups = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $value): int => (int) $value,
|
||||
StringUtil::deserialize($row['groups'] ?? null, true),
|
||||
))));
|
||||
|
||||
if ([] === $calendarGroups || [] === array_intersect($memberGroupIds, $calendarGroups)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$archives[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'title' => $this->normalizeTitle((string) ($row['title'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
return $archives;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $rawValues
|
||||
* @param list<int> $configuredArchiveIds
|
||||
* @param bool $archiveFilterApplied
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
private function resolveSelectedArchiveIds(mixed $rawValues, array $configuredArchiveIds, bool $archiveFilterApplied): array
|
||||
{
|
||||
if ([] === $configuredArchiveIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$archiveFilterApplied) {
|
||||
return $configuredArchiveIds;
|
||||
}
|
||||
|
||||
if (!is_array($rawValues) || [] === $rawValues) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$selected = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $value): int => (int) $value,
|
||||
$rawValues,
|
||||
))));
|
||||
|
||||
return array_values(array_intersect($configuredArchiveIds, $selected));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GymnasiumNossenBundle\Controller;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
#[Route('/events/pdf', name: 'gymnasium_event_pdf', methods: ['POST'])]
|
||||
class EventPdfController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly Environment $twig,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$eventIds = $this->normalizeEventIds($request->request->all('event_ids'));
|
||||
|
||||
if ([] === $eventIds) {
|
||||
return $this->redirectToListWithError($request, 'no_selection');
|
||||
}
|
||||
|
||||
$events = $this->fetchEvents($eventIds);
|
||||
|
||||
if ([] === $events) {
|
||||
return $this->redirectToListWithError($request, 'not_found');
|
||||
}
|
||||
|
||||
$from = $this->parseDateParameter($request->request->get('from'));
|
||||
$to = $this->parseDateParameter($request->request->get('to'));
|
||||
$heading = $this->normalizeOptionalText($request->request->get('pdf_heading')) ?? 'Termine';
|
||||
$introText = $this->normalizeOptionalText($request->request->get('pdf_intro'));
|
||||
|
||||
$html = $this->renderTemplate($events, $from, $to, $heading, $introText);
|
||||
|
||||
$options = new Options();
|
||||
$options->set('defaultFont', 'DejaVu Sans');
|
||||
$options->set('isRemoteEnabled', false);
|
||||
|
||||
$dompdf = new Dompdf($options);
|
||||
$dompdf->loadHtml($html, 'UTF-8');
|
||||
$dompdf->setPaper('A4', 'portrait');
|
||||
$dompdf->render();
|
||||
|
||||
return new Response(
|
||||
$dompdf->output(),
|
||||
Response::HTTP_OK,
|
||||
[
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'attachment; filename="termine.pdf"',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $rawIds
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
private function normalizeEventIds(array $rawIds): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $value): int => (int) $value,
|
||||
$rawIds,
|
||||
))));
|
||||
|
||||
return array_values(array_filter($ids, static fn (int $id): bool => $id > 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $eventIds
|
||||
*
|
||||
* @return list<array{title: string, startText: string, endText: string|null}>
|
||||
*/
|
||||
private function fetchEvents(array $eventIds): array
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('e.title', 'e.addTime', 'e.startDate', 'e.endDate', 'e.startTime', 'e.endTime')
|
||||
->from('tl_calendar_events', 'e')
|
||||
->where('e.id IN (:ids)')
|
||||
->andWhere('e.published = :published')
|
||||
->setParameter('ids', $eventIds, ArrayParameterType::INTEGER)
|
||||
->setParameter('published', 1)
|
||||
->orderBy('e.startTime', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative()
|
||||
;
|
||||
|
||||
$events = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
['startText' => $startText, 'endText' => $endText] = $this->formatEventDateTimeTexts($row);
|
||||
|
||||
$events[] = [
|
||||
'title' => $this->normalizeTitle((string) ($row['title'] ?? '')),
|
||||
'startText' => $startText,
|
||||
'endText' => $endText,
|
||||
];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
private function parseDateParameter(mixed $rawValue): ?DateTimeImmutable
|
||||
{
|
||||
if (!is_string($rawValue) || '' === $rawValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $rawValue);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
|
||||
if (
|
||||
false === $date
|
||||
|| (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0))
|
||||
|| $date->format('Y-m-d') !== $rawValue
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
private function normalizeTitle(string $title): string
|
||||
{
|
||||
$decoded = $title;
|
||||
|
||||
for ($index = 0; $index < 2; ++$index) {
|
||||
$next = html_entity_decode($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
if ($next === $decoded) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decoded = $next;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{title: string, startText: string, endText: string|null}> $events
|
||||
*/
|
||||
private function renderTemplate(array $events, ?DateTimeImmutable $from, ?DateTimeImmutable $to, string $heading, ?string $introText): string
|
||||
{
|
||||
$templatePath = __DIR__.'/../../Resources/views/pdf/events.html.twig';
|
||||
$templateContent = file_get_contents($templatePath);
|
||||
|
||||
if (false === $templateContent) {
|
||||
throw new \RuntimeException('The PDF template file could not be read: '.$templatePath);
|
||||
}
|
||||
|
||||
$dateRange = null;
|
||||
|
||||
if (null !== $from && null !== $to && $from <= $to) {
|
||||
$dateRange = sprintf('%s - %s', $this->formatGermanLongDate($from), $this->formatGermanLongDate($to));
|
||||
}
|
||||
|
||||
$logoDataUri = $this->getLogoDataUri();
|
||||
|
||||
return $this->twig->createTemplate($templateContent)->render([
|
||||
'events' => $events,
|
||||
'heading' => $heading,
|
||||
'introText' => $introText,
|
||||
'dateRange' => $dateRange,
|
||||
'logoDataUri' => $logoDataUri,
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizeOptionalText(mixed $rawValue): ?string
|
||||
{
|
||||
if (!is_string($rawValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($rawValue);
|
||||
|
||||
return '' !== $value ? $value : null;
|
||||
}
|
||||
|
||||
private function redirectToListWithError(Request $request, string $error): RedirectResponse
|
||||
{
|
||||
$target = (string) $request->headers->get('referer', '/');
|
||||
|
||||
$parts = parse_url($target);
|
||||
$query = [];
|
||||
|
||||
if (isset($parts['query']) && '' !== $parts['query']) {
|
||||
parse_str($parts['query'], $query);
|
||||
}
|
||||
|
||||
$query['pdf_error'] = $error;
|
||||
|
||||
$path = ($parts['path'] ?? '/').'?'.http_build_query($query);
|
||||
|
||||
return new RedirectResponse($path, Response::HTTP_SEE_OTHER);
|
||||
}
|
||||
|
||||
private function formatGermanLongDate(DateTimeImmutable $date): string
|
||||
{
|
||||
$months = [
|
||||
1 => 'Januar',
|
||||
2 => 'Februar',
|
||||
3 => 'März',
|
||||
4 => 'April',
|
||||
5 => 'Mai',
|
||||
6 => 'Juni',
|
||||
7 => 'Juli',
|
||||
8 => 'August',
|
||||
9 => 'September',
|
||||
10 => 'Oktober',
|
||||
11 => 'November',
|
||||
12 => 'Dezember',
|
||||
];
|
||||
|
||||
$month = $months[(int) $date->format('n')] ?? $date->format('m');
|
||||
|
||||
return sprintf('%d. %s %d', (int) $date->format('j'), $month, (int) $date->format('Y'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*
|
||||
* @return array{startText: string, endText: string|null}
|
||||
*/
|
||||
private function formatEventDateTimeTexts(array $row): array
|
||||
{
|
||||
$addTime = (int) ($row['addTime'] ?? 0) === 1;
|
||||
$startDateTimestamp = (int) ($row['startDate'] ?? 0);
|
||||
$endDateTimestamp = (int) ($row['endDate'] ?? 0);
|
||||
$startTimeTimestamp = (int) ($row['startTime'] ?? 0);
|
||||
$endTimeTimestamp = (int) ($row['endTime'] ?? 0);
|
||||
|
||||
if ($addTime) {
|
||||
$startTimestamp = $startTimeTimestamp > 0 ? $startTimeTimestamp : $startDateTimestamp;
|
||||
$start = (new DateTimeImmutable())->setTimestamp($startTimestamp);
|
||||
$startText = $start->format('d.m.Y H:i').' Uhr';
|
||||
|
||||
if ($endDateTimestamp <= 0) {
|
||||
return ['startText' => $startText, 'endText' => null];
|
||||
}
|
||||
|
||||
$endTimestamp = $endTimeTimestamp > 0 ? $endTimeTimestamp : $endDateTimestamp;
|
||||
$end = (new DateTimeImmutable())->setTimestamp($endTimestamp);
|
||||
|
||||
$sameDay = $start->format('Y-m-d') === $end->format('Y-m-d');
|
||||
$endText = $sameDay ? $end->format('H:i').' Uhr' : $end->format('d.m.Y H:i').' Uhr';
|
||||
|
||||
return ['startText' => $startText, 'endText' => $endText];
|
||||
}
|
||||
|
||||
$startTimestamp = $startDateTimestamp > 0 ? $startDateTimestamp : $startTimeTimestamp;
|
||||
$start = (new DateTimeImmutable())->setTimestamp($startTimestamp);
|
||||
$startText = $start->format('d.m.Y');
|
||||
|
||||
if ($endDateTimestamp <= 0) {
|
||||
return ['startText' => $startText, 'endText' => null];
|
||||
}
|
||||
|
||||
$end = (new DateTimeImmutable())->setTimestamp($endDateTimestamp);
|
||||
$endText = $end->format('d.m.Y');
|
||||
|
||||
return ['startText' => $startText, 'endText' => $endText !== $startText ? $endText : null];
|
||||
}
|
||||
|
||||
private function getLogoDataUri(): ?string
|
||||
{
|
||||
$logoPath = __DIR__.'/../../Resources/img/2022_06_26_Gymnasium_Nossen_Siegel_schwarz.svg';
|
||||
$logoContent = file_get_contents($logoPath);
|
||||
|
||||
if (false === $logoContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'data:image/svg+xml;base64,'.base64_encode($logoContent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GymnasiumNossenBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
|
||||
|
||||
class GymnasiumNossenExtension extends Extension
|
||||
{
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/contao/config'));
|
||||
$loader->load('services.yaml');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GymnasiumNossenBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class GymnasiumNossenBundle extends Bundle
|
||||
{
|
||||
public function getPath(): string
|
||||
{
|
||||
return dirname(__DIR__);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
gymnasium_nossen_bundle_controllers:
|
||||
resource: ../../Controller/
|
||||
type: attribute
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
GymnasiumNossenBundle\Controller\:
|
||||
resource: ../../../Controller/
|
||||
|
||||
GymnasiumNossenBundle\Controller\EventListModuleController:
|
||||
tags:
|
||||
- { name: contao.frontend_module, type: gymnasium_eventlist, category: events, template: frontend/module_event_list }
|
||||
Reference in New Issue
Block a user