Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.10% covered (danger)
8.10%
29 / 358
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
OlzTerminDetailParams
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
OlzTerminDetail
8.10% covered (danger)
8.10%
29 / 358
16.67% covered (danger)
16.67%
1 / 6
2854.09
0.00% covered (danger)
0.00%
0 / 1
 hasAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 searchSqlWhenHasAccess
80.00% covered (warning)
80.00%
28 / 35
0.00% covered (danger)
0.00%
0 / 1
2.03
 getPageTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHtmlWhenHasAccess
0.00% covered (danger)
0.00%
0 / 315
0.00% covered (danger)
0.00%
0 / 1
2970
 getTerminById
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Olz\Termine\Components\OlzTerminDetail;
4
5use Doctrine\Common\Collections\Criteria;
6use Olz\Common\Components\OlzLocationMap\OlzLocationMap;
7use Olz\Common\Components\OlzRootComponent;
8use Olz\Components\Page\OlzFooter\OlzFooter;
9use Olz\Components\Page\OlzHeader\OlzHeader;
10use Olz\Components\Schema\OlzEventData\OlzEventData;
11use Olz\Entity\News\NewsEntry;
12use Olz\Entity\Termine\Termin;
13use Olz\Entity\Termine\TerminLabel;
14use Olz\Entity\Termine\TerminNotification;
15use Olz\News\Components\OlzNewsListItem\OlzNewsListItem;
16use Olz\Termine\Components\OlzTermineListItem\OlzTermineListItem;
17use Olz\Users\Components\OlzUserInfoModal\OlzUserInfoModal;
18use Olz\Utils\HttpParams;
19
20/** @extends HttpParams<array{von?: ?string}> */
21class OlzTerminDetailParams extends HttpParams {
22}
23
24/** @extends OlzRootComponent<array<string, mixed>> */
25class OlzTerminDetail extends OlzRootComponent {
26    public function hasAccess(): bool {
27        return true;
28    }
29
30    public function searchSqlWhenHasAccess(array $terms): string|array|null {
31        $code_href = $this->envUtils()->getCodeHref();
32        $today_iso = $this->dateUtils()->getIsoToday();
33        $where = implode(' AND ', array_map(function ($term) {
34            $date_sql = '';
35            $term_date_range = $this->dateUtils()->parseDateTimeRange($term);
36            if ($term_date_range !== null) {
37                $query_start = $term_date_range['start']->format('Y-m-d');
38                $query_end = $term_date_range['end']->format('Y-m-d');
39                $date_sql = <<<ZZZZZZZZZZ
40                    OR (
41                        t.start_date < '{$query_end}'
42                        AND IFNULL(t.end_date, t.start_date) >= '{$query_start}'
43                    )
44                    ZZZZZZZZZZ;
45            }
46            return <<<ZZZZZZZZZZ
47                (
48                    t.title LIKE '%{$term}%'
49                    OR t.text LIKE '%{$term}%'
50                    OR tl.name LIKE '%{$term}%'
51                    {$date_sql}
52                )
53                ZZZZZZZZZZ;
54        }, $terms));
55        return [
56            'with' => [
57                <<<ZZZZZZZZZZ
58                    base_termine AS (
59                        SELECT
60                            CONCAT('{$code_href}termine/', t.id) AS link,
61                            '{$code_href}assets/icns/termine_type_all_20.svg' AS icon,
62                            t.start_date AS date,
63                            CONCAT('Termin: ', t.title) AS title,
64                            CONCAT(IFNULL(tl.name, ''), ' ', IFNULL(t.text, ''), ' Typ: ', (
65                                SELECT GROUP_CONCAT(l.name ORDER BY l.position ASC SEPARATOR ', ')
66                                FROM
67                                    termin_label_map tlm
68                                    JOIN termin_labels l ON (l.id = tlm.label_id)
69                                WHERE tlm.termin_id = t.id
70                                GROUP BY t.id
71                            )) AS text,
72                            DATEDIFF(t.start_date, '{$today_iso}') AS diffdays
73                        FROM termine t LEFT JOIN termin_locations tl ON (t.location_id = tl.id)
74                        WHERE
75                            t.on_off = '1'
76                            AND {$this->termineUtils()->getIsNotArchivedSql('t')}
77                            AND {$where}
78                    )
79                    ZZZZZZZZZZ,
80            ],
81            'query' => <<<'ZZZZZZZZZZ'
82                SELECT
83                    link, icon, date, title, text,
84                    CASE
85                        WHEN diffdays < -30 THEN 0.7
86                        WHEN diffdays <= 0 THEN 1.0 + diffdays * 0.3 / 30.0
87                        WHEN diffdays < 100 THEN 1.0
88                        WHEN diffdays < 400 THEN 1.0 - (diffdays - 100) * 0.3 / 300.0
89                        ELSE 0.7
90                    END AS time_relevance
91                FROM base_termine
92                ZZZZZZZZZZ,
93        ];
94    }
95
96    public function getPageTitle(): string {
97        return "";
98    }
99
100    public function getPageDescription(): string {
101        return "";
102    }
103
104    public function getHtmlWhenHasAccess(mixed $args): string {
105        $this->httpUtils()->validateGetParams(OlzTerminDetailParams::class);
106
107        $code_href = $this->envUtils()->getCodeHref();
108        $code_path = $this->envUtils()->getCodePath();
109        $data_path = $this->envUtils()->getDataPath();
110        $date_utils = $this->dateUtils();
111        $today = $date_utils->getIsoToday();
112        $entityManager = $this->dbUtils()->getEntityManager();
113        $user = $this->authUtils()->getCurrentUser();
114        $id = $args['id'] ?? null;
115
116        $termin_repo = $entityManager->getRepository(Termin::class);
117        $is_not_archived = $this->termineUtils()->getIsNotArchivedCriteria();
118        $criteria = Criteria::create()
119            ->where(Criteria::expr()->andX(
120                $is_not_archived,
121                Criteria::expr()->eq('id', $id),
122                Criteria::expr()->eq('on_off', 1),
123            ))
124            ->setFirstResult(0)
125            ->setMaxResults(1)
126        ;
127        $termine = $termin_repo->matching($criteria);
128        $num_termine = $termine->count();
129        $is_archived = $num_termine !== 1;
130
131        if ($is_archived && !$this->authUtils()->hasPermission('any')) {
132            $this->httpUtils()->dieWithHttpError(404);
133            throw new \Exception('should already have failed');
134        }
135
136        $termin = $this->getTerminById($id);
137
138        if (!$termin) {
139            $this->httpUtils()->dieWithHttpError(404);
140            throw new \Exception('should already have failed');
141        }
142
143        $title = $termin->getTitle() ?? '';
144        $termin_year = $termin->getStartDate()->format('Y');
145        $this_year = $this->dateUtils()->getCurrentDateInFormat('Y');
146        $maybe_date = ($termin_year !== $this_year) ? " {$termin_year}" : '';
147        $title = "{$title}{$maybe_date}";
148        $text = $termin->getText() ?? '';
149        $parent_termin = $termin->getParentTermin();
150        $back_link = $parent_termin
151            ? "{$code_href}termine/{$parent_termin->getId()}"
152            : "{$code_href}termine";
153        $out = OlzHeader::render([
154            'back_link' => $back_link,
155            'title' => "{$title} - Termine",
156            'description' => "{$text}",
157            'norobots' => $is_archived,
158            'canonical_url' => "{$code_href}termine/{$id}",
159        ]);
160
161        $out .= "<div class='content-full'>";
162
163        $start_date = $termin->getStartDate();
164        $end_date = $termin->getEndDate() ?? null;
165        $start_time = $termin->getStartTime() ?? null;
166        $end_time = $termin->getEndTime() ?? null;
167        $organizer = $termin->getOrganizerUser();
168        $labels = [...$termin->getLabels()];
169        $latitude = $termin->getLatitude() ?? 0;
170        $longitude = $termin->getLongitude() ?? 0;
171        $solv_uid = $termin->getSolvId();
172        $termin_location = $termin->getLocation();
173        $has_olz_location = ($latitude > 0 && $longitude > 0);
174        $has_termin_location = (
175            $termin_location
176            && $termin_location->getLatitude() > 0
177            && $termin_location->getLongitude() > 0
178        );
179        $lat = null;
180        $lng = null;
181        $location_name = null;
182        if ($has_termin_location) {
183            $lat = $termin_location->getLatitude();
184            $lng = $termin_location->getLongitude();
185            $location_name = $termin_location->getName();
186        }
187        if ($has_olz_location) {
188            $lat = $latitude;
189            $lng = $longitude;
190            $location_name = null;
191        }
192        $has_location = $has_olz_location || $has_termin_location;
193        $image_ids = $termin->getImageIds();
194
195        $out .= OlzEventData::render([
196            'name' => $title,
197            'start_date' => $date_utils->olzDate('jjjj-mm-tt', $start_date),
198            'end_date' => $end_date ? $date_utils->olzDate('jjjj-mm-tt', $end_date) : null,
199            'location' => $has_location ? [
200                'lat' => $lat,
201                'lng' => $lng,
202                'name' => $location_name,
203            ] : null,
204        ]);
205
206        $out .= "<div class='olz-termin-detail'>";
207
208        if ($parent_termin) {
209            $out .= "<div class='parent-termin-container'>";
210            $out .= OlzTermineListItem::render([
211                'id' => $parent_termin->getId(),
212                'owner_user_id' => $parent_termin->getOwnerUser()?->getId(),
213                'start_date' => $parent_termin->getStartDate()->format('Y-m-d'),
214                'start_time' => $parent_termin->getStartTime()?->format('H:i:s'),
215                'end_date' => $parent_termin->getEndDate()?->format('Y-m-d'),
216                'end_time' => $parent_termin->getEndTime()?->format('H:i:s'),
217                'organizer_user_id' => $parent_termin->getOrganizerUser()?->getId(),
218                'title' => $parent_termin->getTitle(),
219                'text' => $parent_termin->getText(),
220                'labels' => $parent_termin->getLabels(),
221                'image_ids' => $parent_termin->getImageIds(),
222                'location_id' => $parent_termin->getLocation()?->getId(),
223            ]);
224            $out .= "</div>";
225        }
226
227        $out .= "<div class='preview'>";
228        // Bild anzeigen
229        if (count($image_ids) > 0) {
230            $out .= $this->imageUtils()->olzImage(
231                'termine',
232                $id,
233                $image_ids[0],
234                840
235            );
236        // Karte zeigen
237        } elseif ($has_location) {
238            $out .= OlzLocationMap::render([
239                'latitude' => $lat,
240                'longitude' => $lng,
241                'zoom' => 13,
242            ]);
243        }
244        // Date & Title
245        $pretty_date = $this->dateUtils()->formatDateTimeRange(
246            $start_date->format('Y-m-d'),
247            $start_time?->format('H:i:s'),
248            $end_date?->format('Y-m-d'),
249            $end_time?->format('H:i:s'),
250            $format = 'long',
251        );
252        $out .= "<div class='title-date-container'>";
253        $esc_title = htmlspecialchars($title);
254        $out .= "<h1>{$esc_title}</h1>";
255        $out .= "<h4>{$pretty_date}</h4>";
256        $out .= "</div>";
257
258        // Editing Tools
259        $is_owner = $user && intval($termin->getOwnerUser()?->getId() ?? 0) === intval($user->getId());
260        $is_organizer = $user && intval($termin->getOrganizerUser()?->getId() ?? 0) === intval($user->getId());
261        $has_termine_permissions = $this->authUtils()->hasPermission('termine');
262        $can_edit = $is_owner || $is_organizer || $has_termine_permissions;
263        $add_termin_notification_admin = '';
264        if ($can_edit) {
265            $json_id = json_encode($id);
266            $out .= <<<ZZZZZZZZZZ
267                <div class='actions-container'>
268                    <button
269                        id='edit-termin-button'
270                        class='btn btn-outline-light'
271                        onclick='return olz.editTermin({$json_id})'
272                    >
273                        <img src='{$code_href}assets/icns/edit_white_16.svg' class='noborder' />
274                        Bearbeiten
275                    </button>
276                </div>
277                ZZZZZZZZZZ;
278            $add_termin_notification_admin = <<<ZZZZZZZZZZ
279                <div>
280                    <button
281                        id='add-termin-notification-button'
282                        class='btn btn-secondary btn-sm'
283                        onclick='return olz.addTerminNotification({$json_id})'
284                    >
285                        <img src='{$code_href}assets/icns/new_white_16.svg' class='noborder' />
286                        Neue Benachrichtigung
287                    </button>
288                </div>
289                ZZZZZZZZZZ;
290        }
291
292        $out .= "</div>"; // preview
293
294        $labels_html = implode('', array_map(function (TerminLabel $label) use ($code_path, $code_href) {
295            $ident = $label->getIdent();
296            $serialized_filter = $this->termineUtils()->serialize([
297                'typ' => $ident,
298                'datum' => 'bevorstehend',
299            ]);
300            // TODO: Remove fallback mechanism?
301            $fallback_path = "{$code_path}assets/icns/termine_type_{$ident}_20.svg";
302            $fallback_href = is_file($fallback_path)
303                ? "{$code_href}assets/icns/termine_type_{$ident}_20.svg" : null;
304            $icon_href = $label->getIcon() ? $label->getFileHref($label->getIcon()) : $fallback_href;
305            $esc_name = htmlspecialchars($label->getName());
306            return $icon_href ? <<<ZZZZZZZZZZ
307                <a href='{$code_href}termine?filter={$serialized_filter}' class='filter'>
308                    <img src='{$icon_href}' alt='' class='type-icon'>{$esc_name}
309                </a>
310                ZZZZZZZZZZ : '';
311        }, $labels));
312        $out .= "<div class='filters'>{$labels_html}</div>";
313
314        if ($organizer) {
315            $pretty_organizer = OlzUserInfoModal::render(['user' => $organizer]);
316            $out .= "<div>Organisator: {$pretty_organizer}</div><br>";
317        }
318
319        if ($solv_uid) {
320            // SOLV-Übersicht-Link zeigen
321            $out .= "<h4><a href='https://www.o-l.ch/cgi-bin/fixtures?&mode=show&unique_id={$solv_uid}' target='_blank' class='linkol'>O-L.ch</a></h4>\n";
322        }
323
324        $sub_termine = $termin_repo->findBy(['parent_termin' => $termin], [
325            'start_date' => 'ASC',
326            'start_time' => 'ASC',
327        ]);
328        foreach ($sub_termine as $sub_termin) {
329            $out .= OlzTermineListItem::render([
330                'id' => $sub_termin->getId(),
331                'owner_user_id' => $sub_termin->getOwnerUser()?->getId(),
332                'start_date' => $sub_termin->getStartDate()->format('Y-m-d'),
333                'start_time' => $sub_termin->getStartTime()?->format('H:i:s'),
334                'end_date' => $sub_termin->getEndDate()?->format('Y-m-d'),
335                'end_time' => $sub_termin->getEndTime()?->format('H:i:s'),
336                'organizer_user_id' => $sub_termin->getOrganizerUser()?->getId(),
337                'title' => $sub_termin->getTitle(),
338                'text' => $sub_termin->getText(),
339                'labels' => $sub_termin->getLabels(),
340                'image_ids' => $sub_termin->getImageIds(),
341                'location_id' => $sub_termin->getLocation()?->getId(),
342            ]);
343        }
344
345        // Text
346        $text_html = $this->htmlUtils()->renderMarkdown($text, [
347            // TODO: Do NOT ever allow!
348            'html_input' => $start_date->format('Y') > '2020' ? 'escape' : 'allow',
349        ]);
350        $text_html = $termin->replaceImagePaths($text_html);
351        $text_html = $termin->replaceFilePaths($text_html);
352        if ($termin->getDeadline() && $termin->getDeadline() != "0000-00-00") {
353            $text_html .= ($text_html == "" ? "" : "<br />")."Meldeschluss: ".$date_utils->olzDate("t. MM ", $termin->getDeadline());
354        }
355        $out .= "<div class='text'>".$text_html."</div>";
356
357        // Link
358        $link = '';
359        if ($solv_uid && $start_date->format('Y-m-d') <= $today && !preg_match('/(Rangliste|Resultat)/', $link)) {
360            // SOLV Ranglisten-Link zeigen
361            $link .= "<div><a href='http://www.o-l.ch/cgi-bin/results?unique_id={$solv_uid}&club=zimmerberg' target='_blank' class='linkol'>Rangliste</a></div>\n";
362        }
363        $result_filename = "{$termin_year}-termine-{$id}.xml";
364        if (is_file("{$data_path}results/{$result_filename}")) {
365            // OLZ Ranglisten-Link zeigen
366            $link .= "<div><a href='{$code_href}apps/resultate?file={$result_filename}' target='_blank' class='linkext'>Ranglisten</a></div>\n";
367        } elseif ($can_edit) {
368            // OLZ Rangliste-hochladen-Link zeigen
369            $link .= "<div><a href='{$code_href}apps/resultate?file={$result_filename}' target='_blank' class='linkext'>Rangliste hochladen</a></div>\n";
370        }
371        $link = str_replace("&", "&amp;", str_replace("&amp;", "&", $link));
372        $link = str_replace("www.solv.ch", "www.o-l.ch", $link);
373        $out .= "<div class='links'>".$link."</div>";
374
375        // Karte zeigen
376        if ($has_location) {
377            if ($location_name !== null) {
378                $esc_location_name = htmlspecialchars($location_name);
379                $location_maybe_link = $esc_location_name;
380                if ($this->authUtils()->hasPermission('termine')) {
381                    $location_maybe_link = "<a href='{$code_href}termine/orte/{$termin_location->getId()}' class='linkmap'>{$esc_location_name}</a>";
382                }
383                $out .= "<h3>Ort: {$location_maybe_link}</h3>";
384            } else {
385                $out .= "<h3>Ort</h3>";
386            }
387            $out .= OlzLocationMap::render([
388                'name' => $location_name ?? '',
389                'latitude' => $lat,
390                'longitude' => $lng,
391                'zoom' => 13,
392            ]);
393            if ($has_termin_location) {
394                $details = $termin_location->getDetails() ?? '';
395                $details_html = $this->htmlUtils()->renderMarkdown($details);
396                $details_html = $termin_location->replaceImagePaths($details_html);
397                $details_html = $termin_location->replaceFilePaths($details_html);
398                $out .= $details_html;
399
400                $location_image_ids = $termin_location->getImageIds();
401                if (count($location_image_ids) > 0) {
402                    $out .= "<div class='lightgallery gallery-container'>";
403                    foreach ($location_image_ids as $image_id) {
404                        $out .= "<div class='gallery-image'>";
405                        $out .= $this->imageUtils()->olzImage(
406                            'termin_locations',
407                            $termin_location->getId(),
408                            $image_id,
409                            128,
410                            'gallery[myset]'
411                        );
412                        $out .= "</div>";
413                    }
414                    $out .= "</div>";
415                }
416            }
417        }
418
419        // Reactions
420        $json_id = json_encode($id);
421        $linked_reactions = $this->htmlUtils()->getLinkedReactions($text_html);
422        $json_reactions = json_encode($linked_reactions) ?: '[]';
423        $out .= "<div id='termin-reactions'></div><script>olz.initTerminReactions({$json_id}, {$json_reactions});</script>";
424
425        // News
426        $has_blog = $this->authUtils()->hasPermission('kaderblog', $user);
427        $has_roles = !empty($this->authUtils()->getAuthenticatedRoles());
428        $json_mode = htmlentities(json_encode($has_roles ? ($has_blog ? 'account_with_all' : 'account_with_aktuell') : ($has_blog ? 'account_with_blog' : 'account')) ?: '');
429        $add_termin_news_admin = '';
430        if ($this->authUtils()->hasPermission('any')) {
431            $add_termin_news_admin = <<<ZZZZZZZZZZ
432                <button
433                    id='add-termin-news-button'
434                    class='btn btn-secondary btn-sm'
435                    onclick='return olz.addTerminNewsEntry({$json_id}, {$json_mode})'
436                >
437                    <img src='{$code_href}assets/icns/new_white_16.svg' class='noborder' />
438                    Neuer News-Eintrag
439                </button>
440                ZZZZZZZZZZ;
441        }
442        $out .= "<div id='termin-news'>";
443        $news_repo = $this->entityManager()->getRepository(NewsEntry::class);
444        $news_entries = $news_repo->findBy(
445            ['termin' => $termin],
446            [
447                'published_date' => 'DESC',
448                'published_time' => 'DESC',
449            ],
450        );
451        $num_news_entries = count($news_entries);
452        $out .= $add_termin_news_admin;
453        if ($num_news_entries === 0) {
454            $out .= "<p><i>Keine News-Einträge</i></p>";
455        } else {
456            foreach ($news_entries as $news_entry) {
457                $out .= OlzNewsListItem::render([
458                    'json_mode' => $json_mode,
459                    'news_entry' => $news_entry,
460                ]);
461            }
462        }
463        $out .= "</div>";
464
465        // Termin Notifications
466        if ($can_edit) {
467            $termin_notification_repo = $this->entityManager()->getRepository(TerminNotification::class);
468            $termin_notifications = $termin_notification_repo->findBy(
469                ['termin' => $termin],
470                ['fires_earlier_seconds' => 'DESC'],
471            );
472            $num_termin_notifications = count($termin_notifications);
473            $out .= "<div id='termin-notifications'>";
474            if ($num_termin_notifications === 0) {
475                $out .= "<p><i>Keine Benachrichtigungen</i></p>";
476                $out .= $add_termin_notification_admin;
477            } else {
478                $out .= $add_termin_notification_admin;
479                $out .= "<table id='termin-notifications-table' class='boxy'>";
480                $out .= "<tr><th>Zeitpunkt</th><th>Titel</th><th>Empfänger</th></tr>";
481                foreach ($termin_notifications as $termin_notification) {
482                    $out .= "<tr class='termin-notification'>";
483                    $json_id = json_encode(intval($termin_notification->getId()));
484                    $row_admin = <<<ZZZZZZZZZZ
485                        <button
486                            id='edit-termin-notification-button-{$json_id}'
487                            class='btn btn-secondary-outline btn-sm edit-termin-notification-button'
488                            onclick='return olz.editTerminNotification({$json_id})'
489                        >
490                            <img src='{$code_href}assets/icns/edit_16.svg' class='noborder' />
491                        </button>
492                        ZZZZZZZZZZ;
493                    $pretty_fires_earlier = $this->dateUtils()->formatDateInterval(
494                        $termin_notification->getFiresEarlierSeconds()
495                    );
496                    $pretty_recipients = implode('', array_map(
497                        fn ($recipient) => "<div>".htmlspecialchars($recipient)."</div>",
498                        $this->termineUtils()->formatNotificationRecipients($termin_notification),
499                    ));
500                    $out .= "<td>{$row_admin}{$pretty_fires_earlier} vorher</td>";
501                    $out .= "<td><b>".htmlspecialchars($termin_notification->getTitle())."</b></td>";
502                    $out .= "<td>{$pretty_recipients}</td>";
503                    $out .= "</tr>";
504                }
505                $out .= "</table>";
506            }
507            $out .= "</div>";
508        }
509
510        $out .= "</div>"; // olz-termin-detail
511        $out .= "</div>"; // content-full
512
513        $out .= OlzFooter::render();
514
515        return $out;
516    }
517
518    protected function getTerminById(int $id): ?Termin {
519        $termin_repo = $this->entityManager()->getRepository(Termin::class);
520        return $termin_repo->findOneBy([
521            'id' => $id,
522            'on_off' => 1,
523        ]);
524    }
525}