Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.48% covered (danger)
9.48%
29 / 306
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
OlzTerminDetailParams
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
OlzTerminDetail
9.48% covered (danger)
9.48%
29 / 306
14.29% covered (danger)
14.29%
1 / 7
2821.16
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 / 260
0.00% covered (danger)
0.00%
0 / 1
2756
 getTerminById
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getTimeText
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
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\Termine\Termin;
12use Olz\Entity\Termine\TerminLabel;
13use Olz\Entity\Termine\TerminNotification;
14use Olz\Termine\Components\OlzDateCalendar\OlzDateCalendar;
15use Olz\Users\Components\OlzUserInfoModal\OlzUserInfoModal;
16use Olz\Utils\HttpParams;
17
18/** @extends HttpParams<array{von?: ?string}> */
19class OlzTerminDetailParams extends HttpParams {
20}
21
22/** @extends OlzRootComponent<array<string, mixed>> */
23class OlzTerminDetail extends OlzRootComponent {
24    public function hasAccess(): bool {
25        return true;
26    }
27
28    public function searchSqlWhenHasAccess(array $terms): string|array|null {
29        $code_href = $this->envUtils()->getCodeHref();
30        $today_iso = $this->dateUtils()->getIsoToday();
31        $where = implode(' AND ', array_map(function ($term) {
32            $date_sql = '';
33            $term_date_range = $this->dateUtils()->parseDateTimeRange($term);
34            if ($term_date_range !== null) {
35                $query_start = $term_date_range['start']->format('Y-m-d');
36                $query_end = $term_date_range['end']->format('Y-m-d');
37                $date_sql = <<<ZZZZZZZZZZ
38                    OR (
39                        t.start_date < '{$query_end}'
40                        AND IFNULL(t.end_date, t.start_date) >= '{$query_start}'
41                    )
42                    ZZZZZZZZZZ;
43            }
44            return <<<ZZZZZZZZZZ
45                (
46                    t.title LIKE '%{$term}%'
47                    OR t.text LIKE '%{$term}%'
48                    OR tl.name LIKE '%{$term}%'
49                    {$date_sql}
50                )
51                ZZZZZZZZZZ;
52        }, $terms));
53        return [
54            'with' => [
55                <<<ZZZZZZZZZZ
56                    base_termine AS (
57                        SELECT
58                            CONCAT('{$code_href}termine/', t.id) AS link,
59                            '{$code_href}assets/icns/termine_type_all_20.svg' AS icon,
60                            t.start_date AS date,
61                            CONCAT('Termin: ', t.title) AS title,
62                            CONCAT(IFNULL(tl.name, ''), ' ', IFNULL(t.text, ''), ' Typ: ', (
63                                SELECT GROUP_CONCAT(l.name ORDER BY l.position ASC SEPARATOR ', ')
64                                FROM
65                                    termin_label_map tlm
66                                    JOIN termin_labels l ON (l.id = tlm.label_id)
67                                WHERE tlm.termin_id = t.id
68                                GROUP BY t.id
69                            )) AS text,
70                            DATEDIFF(t.start_date, '{$today_iso}') AS diffdays
71                        FROM termine t LEFT JOIN termin_locations tl ON (t.location_id = tl.id)
72                        WHERE
73                            t.on_off = '1'
74                            AND {$this->termineUtils()->getIsNotArchivedSql('t')}
75                            AND {$where}
76                    )
77                    ZZZZZZZZZZ,
78            ],
79            'query' => <<<'ZZZZZZZZZZ'
80                SELECT
81                    link, icon, date, title, text,
82                    CASE
83                        WHEN diffdays < -30 THEN 0.7
84                        WHEN diffdays <= 0 THEN 1.0 + diffdays * 0.3 / 30.0
85                        WHEN diffdays < 100 THEN 1.0
86                        WHEN diffdays < 400 THEN 1.0 - (diffdays - 100) * 0.3 / 300.0
87                        ELSE 0.7
88                    END AS time_relevance
89                FROM base_termine
90                ZZZZZZZZZZ,
91        ];
92    }
93
94    public function getPageTitle(): string {
95        return "";
96    }
97
98    public function getPageDescription(): string {
99        return "";
100    }
101
102    public function getHtmlWhenHasAccess(mixed $args): string {
103        $this->httpUtils()->validateGetParams(OlzTerminDetailParams::class);
104
105        $code_href = $this->envUtils()->getCodeHref();
106        $code_path = $this->envUtils()->getCodePath();
107        $data_path = $this->envUtils()->getDataPath();
108        $date_utils = $this->dateUtils();
109        $today = $date_utils->getIsoToday();
110        $entityManager = $this->dbUtils()->getEntityManager();
111        $user = $this->authUtils()->getCurrentUser();
112        $id = $args['id'] ?? null;
113
114        $termin_repo = $entityManager->getRepository(Termin::class);
115        $is_not_archived = $this->termineUtils()->getIsNotArchivedCriteria();
116        $criteria = Criteria::create()
117            ->where(Criteria::expr()->andX(
118                $is_not_archived,
119                Criteria::expr()->eq('id', $id),
120                Criteria::expr()->eq('on_off', 1),
121            ))
122            ->setFirstResult(0)
123            ->setMaxResults(1)
124        ;
125        $termine = $termin_repo->matching($criteria);
126        $num_termine = $termine->count();
127        $is_archived = $num_termine !== 1;
128
129        if ($is_archived && !$this->authUtils()->hasPermission('any')) {
130            $this->httpUtils()->dieWithHttpError(404);
131            throw new \Exception('should already have failed');
132        }
133
134        $termin = $this->getTerminById($id);
135
136        if (!$termin) {
137            $this->httpUtils()->dieWithHttpError(404);
138            throw new \Exception('should already have failed');
139        }
140
141        $title = $termin->getTitle() ?? '';
142        $termin_year = $termin->getStartDate()->format('Y');
143        $this_year = $this->dateUtils()->getCurrentDateInFormat('Y');
144        $maybe_date = ($termin_year !== $this_year) ? " {$termin_year}" : '';
145        $title = "{$title}{$maybe_date}";
146        $text = $termin->getText() ?? '';
147        $out = OlzHeader::render([
148            'back_link' => "{$code_href}termine",
149            'title' => "{$title} - Termine",
150            'description' => "{$text}",
151            'norobots' => $is_archived,
152            'canonical_url' => "{$code_href}termine/{$id}",
153        ]);
154
155        $out .= <<<'ZZZZZZZZZZ'
156            <div class='content-right optional'>
157                <div style='padding:4px 3px 10px 3px;'>
158                </div>
159            </div>
160            <div class='content-middle'>
161            ZZZZZZZZZZ;
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        $out .= "<div class='preview'>";
209        // Bild anzeigen
210        if (count($image_ids) > 0) {
211            $out .= $this->imageUtils()->olzImage(
212                'termine',
213                $id,
214                $image_ids[0],
215                840
216            );
217        // Karte zeigen
218        } elseif ($has_location) {
219            $out .= OlzLocationMap::render([
220                'latitude' => $lat,
221                'longitude' => $lng,
222                'zoom' => 13,
223            ]);
224        }
225        // Date Calendar Icon
226        $out .= "<div class='date-calendar-container'>";
227        $out .= "<div class='date-calendars'>";
228        $out .= "<div class='date-calendar'>";
229        $out .= OlzDateCalendar::render(['date' => $start_date]);
230        $out .= $this->getTimeText($start_time) ?? '';
231        $out .= ($end_time && (!$end_date || $end_date === $start_date))
232            ? ' &ndash; '.$this->getTimeText($end_time)
233            : '';
234        $out .= "</div>";
235        $out .= "<div class='date-calendar'>";
236        $out .= ($end_date && $end_date !== $start_date)
237            ? OlzDateCalendar::render(['date' => $end_date])
238            : '';
239        $out .= ($end_time && $end_date && $end_date !== $start_date)
240            ? $this->getTimeText($end_time)
241            : '';
242        $out .= "</div>";
243        $out .= "</div>";
244        $out .= "</div>";
245
246        $out .= "</div>";
247
248        // Editing Tools
249        $is_owner = $user && intval($termin->getOwnerUser()?->getId() ?? 0) === intval($user->getId());
250        $is_organizer = $user && intval($termin->getOrganizerUser()?->getId() ?? 0) === intval($user->getId());
251        $has_termine_permissions = $this->authUtils()->hasPermission('termine');
252        $can_edit = $is_owner || $is_organizer || $has_termine_permissions;
253        $add_termin_notification_admin = '';
254        if ($can_edit) {
255            $json_id = json_encode($id);
256            $out .= <<<ZZZZZZZZZZ
257                <div>
258                    <button
259                        id='edit-termin-button'
260                        class='btn btn-primary'
261                        onclick='return olz.editTermin({$json_id})'
262                    >
263                        <img src='{$code_href}assets/icns/edit_white_16.svg' class='noborder' />
264                        Bearbeiten
265                    </button>
266                </div>
267                ZZZZZZZZZZ;
268            $add_termin_notification_admin = <<<ZZZZZZZZZZ
269                <div>
270                    <button
271                        id='add-termin-notification-button'
272                        class='btn btn-primary'
273                        onclick='return olz.addTerminNotification({$json_id})'
274                    >
275                        <img src='{$code_href}assets/icns/new_white_16.svg' class='noborder' />
276                        Neue Benachrichtigung
277                    </button>
278                </div>
279                ZZZZZZZZZZ;
280        }
281
282        // Date & Title
283        $pretty_date = $this->dateUtils()->formatDateTimeRange(
284            $start_date->format('Y-m-d'),
285            $start_time?->format('H:i:s'),
286            $end_date?->format('Y-m-d'),
287            $end_time?->format('H:i:s'),
288            $format = 'long',
289        );
290        $maybe_solv_link = '';
291        if ($solv_uid) {
292            // SOLV-Übersicht-Link zeigen
293            $maybe_solv_link .= "<a href='https://www.o-l.ch/cgi-bin/fixtures?&mode=show&unique_id={$solv_uid}' target='_blank' class='linkol' style='margin-left: 20px; font-weight: normal;'>O-L.ch</a>\n";
294        }
295        $labels_html = implode('', array_map(function (TerminLabel $label) use ($code_path, $code_href) {
296            $ident = $label->getIdent();
297            $serialized_filter = $this->termineUtils()->serialize([
298                'typ' => $ident,
299                'datum' => 'bevorstehend',
300            ]);
301            // TODO: Remove fallback mechanism?
302            $fallback_path = "{$code_path}assets/icns/termine_type_{$ident}_20.svg";
303            $fallback_href = is_file($fallback_path)
304                ? "{$code_href}assets/icns/termine_type_{$ident}_20.svg" : null;
305            $icon_href = $label->getIcon() ? $label->getFileHref($label->getIcon()) : $fallback_href;
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'>{$label->getName()}
309                </a>
310                ZZZZZZZZZZ : '';
311        }, $labels));
312        $out .= "<h5>{$pretty_date}{$maybe_solv_link}</h5>";
313        $out .= "<h1>{$title}</h1>";
314        $out .= "<div class='filters'>{$labels_html}</div>";
315        if ($organizer) {
316            $pretty_organizer = OlzUserInfoModal::render(['user' => $organizer]);
317            $out .= "<div>Organisator: {$pretty_organizer}</div><br>";
318        }
319
320        // Text
321        $text_html = $this->htmlUtils()->renderMarkdown($text, [
322            // TODO: Do NOT ever allow!
323            'html_input' => $start_date->format('Y') > '2020' ? 'escape' : 'allow',
324        ]);
325        $text_html = $termin->replaceImagePaths($text_html);
326        $text_html = $termin->replaceFilePaths($text_html);
327        if ($termin->getDeadline() && $termin->getDeadline() != "0000-00-00") {
328            $text_html .= ($text_html == "" ? "" : "<br />")."Meldeschluss: ".$date_utils->olzDate("t. MM ", $termin->getDeadline());
329        }
330        $out .= "<div class='text'>".$text_html."</div>";
331
332        // Link
333        $link = '';
334        if ($solv_uid && $start_date->format('Y-m-d') <= $today && !preg_match('/(Rangliste|Resultat)/', $link)) {
335            // SOLV Ranglisten-Link zeigen
336            $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";
337        }
338        $result_filename = "{$termin_year}-termine-{$id}.xml";
339        if (is_file("{$data_path}results/{$result_filename}")) {
340            // OLZ Ranglisten-Link zeigen
341            $link .= "<div><a href='{$code_href}apps/resultate?file={$result_filename}' target='_blank' class='linkext'>Ranglisten</a></div>\n";
342        } elseif ($can_edit) {
343            // OLZ Rangliste-hochladen-Link zeigen
344            $link .= "<div><a href='{$code_href}apps/resultate?file={$result_filename}' target='_blank' class='linkext'>Rangliste hochladen</a></div>\n";
345        }
346        $link = str_replace("&", "&amp;", str_replace("&amp;", "&", $link));
347        $link = str_replace("www.solv.ch", "www.o-l.ch", $link);
348        $out .= "<div class='links'>".$link."</div>";
349
350        // Karte zeigen
351        if ($has_location) {
352            if ($location_name !== null) {
353                $location_maybe_link = $location_name;
354                if ($this->authUtils()->hasPermission('termine')) {
355                    $location_maybe_link = "<a href='{$code_href}termine/orte/{$termin_location->getId()}' class='linkmap'>{$location_name}</a>";
356                }
357                $out .= "<h3>Ort: {$location_maybe_link}</h3>";
358            } else {
359                $out .= "<h3>Ort</h3>";
360            }
361            $out .= OlzLocationMap::render([
362                'name' => $location_name ?? '',
363                'latitude' => $lat,
364                'longitude' => $lng,
365                'zoom' => 13,
366            ]);
367            if ($has_termin_location) {
368                $details = $termin_location->getDetails() ?? '';
369                $details_html = $this->htmlUtils()->renderMarkdown($details);
370                $details_html = $termin_location->replaceImagePaths($details_html);
371                $details_html = $termin_location->replaceFilePaths($details_html);
372                $out .= $details_html;
373
374                $location_image_ids = $termin_location->getImageIds();
375                if (count($location_image_ids) > 0) {
376                    $out .= "<div class='lightgallery gallery-container'>";
377                    foreach ($location_image_ids as $image_id) {
378                        $out .= "<div class='gallery-image'>";
379                        $out .= $this->imageUtils()->olzImage(
380                            'termin_locations',
381                            $termin_location->getId(),
382                            $image_id,
383                            128,
384                            'gallery[myset]'
385                        );
386                        $out .= "</div>";
387                    }
388                    $out .= "</div>";
389                }
390            }
391        }
392
393        // Reactions
394        $json_id = json_encode($id);
395        $linked_reactions = $this->htmlUtils()->getLinkedReactions($text_html);
396        $json_reactions = json_encode($linked_reactions) ?: '[]';
397        $out .= "<div id='termin-reactions'></div><script>olz.initTerminReactions({$json_id}, {$json_reactions});</script>";
398
399        if ($can_edit) {
400            // Termin Notifications
401            $termin_notification_repo = $this->entityManager()->getRepository(TerminNotification::class);
402            $termin_notifications = $termin_notification_repo->findBy(
403                ['termin' => $termin],
404                ['fires_earlier_seconds' => 'DESC'],
405            );
406            $num_termin_notifications = count($termin_notifications);
407            $out .= "<div id='termin-notifications'>";
408            if ($num_termin_notifications === 0) {
409                $out .= "<p><i>Keine Benachrichtigungen</i></p>";
410                $out .= $add_termin_notification_admin;
411            } else {
412                $out .= $add_termin_notification_admin;
413                $out .= "<table id='termin-notifications-table' class='boxy'>";
414                $out .= "<tr><th>Zeitpunkt</th><th>Titel</th><th>Empfänger</th></tr>";
415                foreach ($termin_notifications as $termin_notification) {
416                    $out .= "<tr class='termin-notification'>";
417                    $json_id = json_encode(intval($termin_notification->getId()));
418                    $row_admin = <<<ZZZZZZZZZZ
419                        <button
420                            id='edit-termin-notification-button-{$json_id}'
421                            class='btn btn-secondary-outline btn-sm edit-termin-notification-button'
422                            onclick='return olz.editTerminNotification({$json_id})'
423                        >
424                            <img src='{$code_href}assets/icns/edit_16.svg' class='noborder' />
425                        </button>
426                        ZZZZZZZZZZ;
427                    $pretty_fires_earlier = $this->dateUtils()->formatDateInterval(
428                        $termin_notification->getFiresEarlierSeconds()
429                    );
430                    $pretty_recipients = implode('', array_map(
431                        fn ($recipient) => "<div>{$recipient}</div>",
432                        $this->termineUtils()->formatNotificationRecipients($termin_notification),
433                    ));
434                    $out .= "<td>{$row_admin}{$pretty_fires_earlier} vorher</td>";
435                    $out .= "<td><b>{$termin_notification->getTitle()}</b></td>";
436                    $out .= "<td>{$pretty_recipients}</td>";
437                    $out .= "</tr>";
438                }
439                $out .= "</table>";
440            }
441            $out .= "</div>";
442        }
443
444        $out .= "</div>"; // olz-termin-detail
445        $out .= "</div>"; // content-middle
446
447        $out .= OlzFooter::render();
448
449        return $out;
450    }
451
452    protected function getTerminById(int $id): ?Termin {
453        $termin_repo = $this->entityManager()->getRepository(Termin::class);
454        return $termin_repo->findOneBy([
455            'id' => $id,
456            'on_off' => 1,
457        ]);
458    }
459
460    protected function getTimeText(?\DateTime $time): ?string {
461        if (!$time || $time->format('H:i:s') === '00:00:00') {
462            return null;
463        }
464        return $time->format('H:i');
465    }
466}