Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.28% covered (danger)
11.28%
29 / 257
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
11.28% covered (danger)
11.28%
29 / 257
14.29% covered (danger)
14.29%
1 / 7
2406.88
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 / 211
0.00% covered (danger)
0.00%
0 / 1
2450
 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\Components\Common\OlzLocationMap\OlzLocationMap;
7use Olz\Components\Common\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\Termine\Components\OlzDateCalendar\OlzDateCalendar;
14use Olz\Users\Components\OlzUserInfoModal\OlzUserInfoModal;
15use Olz\Utils\HttpParams;
16
17/** @extends HttpParams<array{von?: ?string}> */
18class OlzTerminDetailParams extends HttpParams {
19}
20
21/** @extends OlzRootComponent<array<string, mixed>> */
22class OlzTerminDetail extends OlzRootComponent {
23    public function hasAccess(): bool {
24        return true;
25    }
26
27    public function searchSqlWhenHasAccess(array $terms): string|array|null {
28        $code_href = $this->envUtils()->getCodeHref();
29        $today_iso = $this->dateUtils()->getIsoToday();
30        $where = implode(' AND ', array_map(function ($term) {
31            $date_sql = '';
32            $term_date_range = $this->dateUtils()->parseDateTimeRange($term);
33            if ($term_date_range !== null) {
34                $query_start = $term_date_range['start']->format('Y-m-d');
35                $query_end = $term_date_range['end']->format('Y-m-d');
36                $date_sql = <<<ZZZZZZZZZZ
37                    OR (
38                        t.start_date < '{$query_end}'
39                        AND IFNULL(t.end_date, t.start_date) >= '{$query_start}'
40                    )
41                    ZZZZZZZZZZ;
42            }
43            return <<<ZZZZZZZZZZ
44                (
45                    t.title LIKE '%{$term}%'
46                    OR t.text LIKE '%{$term}%'
47                    OR tl.name LIKE '%{$term}%'
48                    {$date_sql}
49                )
50                ZZZZZZZZZZ;
51        }, $terms));
52        return [
53            'with' => [
54                <<<ZZZZZZZZZZ
55                    base_termine AS (
56                        SELECT
57                            CONCAT('{$code_href}termine/', t.id) AS link,
58                            '{$code_href}assets/icns/termine_type_all_20.svg' AS icon,
59                            t.start_date AS date,
60                            CONCAT('Termin: ', t.title) AS title,
61                            CONCAT(IFNULL(tl.name, ''), ' ', IFNULL(t.text, ''), ' Typ: ', (
62                                SELECT GROUP_CONCAT(l.name ORDER BY l.position ASC SEPARATOR ', ')
63                                FROM
64                                    termin_label_map tlm
65                                    JOIN termin_labels l ON (l.id = tlm.label_id)
66                                WHERE tlm.termin_id = t.id
67                                GROUP BY t.id
68                            )) AS text,
69                            DATEDIFF(t.start_date, '{$today_iso}') AS diffdays
70                        FROM termine t LEFT JOIN termin_locations tl ON (t.location_id = tl.id)
71                        WHERE
72                            t.on_off = '1'
73                            AND {$this->termineUtils()->getIsNotArchivedSql('t')}
74                            AND {$where}
75                    )
76                    ZZZZZZZZZZ,
77            ],
78            'query' => <<<'ZZZZZZZZZZ'
79                SELECT
80                    link, icon, date, title, text,
81                    CASE
82                        WHEN diffdays < -30 THEN 0.7
83                        WHEN diffdays <= 0 THEN 1.0 + diffdays * 0.3 / 30.0
84                        WHEN diffdays < 100 THEN 1.0
85                        WHEN diffdays < 400 THEN 1.0 - (diffdays - 100) * 0.3 / 300.0
86                        ELSE 0.7
87                    END AS time_relevance
88                FROM base_termine
89                ZZZZZZZZZZ,
90        ];
91    }
92
93    public function getPageTitle(): string {
94        return "";
95    }
96
97    public function getPageDescription(): string {
98        return "";
99    }
100
101    public function getHtmlWhenHasAccess(mixed $args): string {
102        $this->httpUtils()->validateGetParams(OlzTerminDetailParams::class);
103
104        $code_href = $this->envUtils()->getCodeHref();
105        $code_path = $this->envUtils()->getCodePath();
106        $data_path = $this->envUtils()->getDataPath();
107        $date_utils = $this->dateUtils();
108        $today = $date_utils->getIsoToday();
109        $entityManager = $this->dbUtils()->getEntityManager();
110        $user = $this->authUtils()->getCurrentUser();
111        $id = $args['id'] ?? null;
112
113        $termin_repo = $entityManager->getRepository(Termin::class);
114        $is_not_archived = $this->termineUtils()->getIsNotArchivedCriteria();
115        $criteria = Criteria::create()
116            ->where(Criteria::expr()->andX(
117                $is_not_archived,
118                Criteria::expr()->eq('id', $id),
119                Criteria::expr()->eq('on_off', 1),
120            ))
121            ->setFirstResult(0)
122            ->setMaxResults(1)
123        ;
124        $termine = $termin_repo->matching($criteria);
125        $num_termine = $termine->count();
126        $is_archived = $num_termine !== 1;
127
128        if ($is_archived && !$this->authUtils()->hasPermission('any')) {
129            $this->httpUtils()->dieWithHttpError(404);
130            throw new \Exception('should already have failed');
131        }
132
133        $termin = $this->getTerminById($id);
134
135        if (!$termin) {
136            $this->httpUtils()->dieWithHttpError(404);
137            throw new \Exception('should already have failed');
138        }
139
140        $title = $termin->getTitle() ?? '';
141        $termin_year = $termin->getStartDate()->format('Y');
142        $this_year = $this->dateUtils()->getCurrentDateInFormat('Y');
143        $maybe_date = ($termin_year !== $this_year) ? " {$termin_year}" : '';
144        $title = "{$title}{$maybe_date}";
145        $text = $termin->getText() ?? '';
146        $out = OlzHeader::render([
147            'back_link' => "{$code_href}termine",
148            'title' => "{$title} - Termine",
149            'description' => "{$text}",
150            'norobots' => $is_archived,
151            'canonical_url' => "{$code_href}termine/{$id}",
152        ]);
153
154        $out .= <<<'ZZZZZZZZZZ'
155            <div class='content-right optional'>
156                <div style='padding:4px 3px 10px 3px;'>
157                </div>
158            </div>
159            <div class='content-middle'>
160            ZZZZZZZZZZ;
161
162        $start_date = $termin->getStartDate();
163        $end_date = $termin->getEndDate() ?? null;
164        $start_time = $termin->getStartTime() ?? null;
165        $end_time = $termin->getEndTime() ?? null;
166        $organizer = $termin->getOrganizerUser();
167        $labels = [...$termin->getLabels()];
168        $latitude = $termin->getLatitude() ?? 0;
169        $longitude = $termin->getLongitude() ?? 0;
170        $solv_uid = $termin->getSolvId();
171        $termin_location = $termin->getLocation();
172        $has_olz_location = ($latitude > 0 && $longitude > 0);
173        $has_termin_location = (
174            $termin_location
175            && $termin_location->getLatitude() > 0
176            && $termin_location->getLongitude() > 0
177        );
178        $lat = null;
179        $lng = null;
180        $location_name = null;
181        if ($has_termin_location) {
182            $lat = $termin_location->getLatitude();
183            $lng = $termin_location->getLongitude();
184            $location_name = $termin_location->getName();
185        }
186        if ($has_olz_location) {
187            $lat = $latitude;
188            $lng = $longitude;
189            $location_name = null;
190        }
191        $has_location = $has_olz_location || $has_termin_location;
192        $image_ids = $termin->getImageIds();
193
194        $out .= OlzEventData::render([
195            'name' => $title,
196            'start_date' => $date_utils->olzDate('jjjj-mm-tt', $start_date),
197            'end_date' => $end_date ? $date_utils->olzDate('jjjj-mm-tt', $end_date) : null,
198            'location' => $has_location ? [
199                'lat' => $lat,
200                'lng' => $lng,
201                'name' => $location_name,
202            ] : null,
203        ]);
204
205        $out .= "<div class='olz-termin-detail'>";
206
207        $out .= "<div class='preview'>";
208        // Bild anzeigen
209        if (count($image_ids) > 0) {
210            $out .= $this->imageUtils()->olzImage(
211                'termine',
212                $id,
213                $image_ids[0],
214                840
215            );
216        // Karte zeigen
217        } elseif ($has_location) {
218            $out .= OlzLocationMap::render([
219                'latitude' => $lat,
220                'longitude' => $lng,
221                'zoom' => 13,
222            ]);
223        }
224        // Date Calendar Icon
225        $out .= "<div class='date-calendar-container'>";
226        $out .= "<div class='date-calendars'>";
227        $out .= "<div class='date-calendar'>";
228        $out .= OlzDateCalendar::render(['date' => $start_date]);
229        $out .= $this->getTimeText($start_time) ?? '';
230        $out .= ($end_time && (!$end_date || $end_date === $start_date))
231            ? ' &ndash; '.$this->getTimeText($end_time)
232            : '';
233        $out .= "</div>";
234        $out .= "<div class='date-calendar'>";
235        $out .= ($end_date && $end_date !== $start_date)
236            ? OlzDateCalendar::render(['date' => $end_date])
237            : '';
238        $out .= ($end_time && $end_date && $end_date !== $start_date)
239            ? $this->getTimeText($end_time)
240            : '';
241        $out .= "</div>";
242        $out .= "</div>";
243        $out .= "</div>";
244
245        $out .= "</div>";
246
247        // Editing Tools
248        $is_owner = $user && intval($termin->getOwnerUser()?->getId() ?? 0) === intval($user->getId());
249        $has_termine_permissions = $this->authUtils()->hasPermission('termine');
250        $can_edit = $is_owner || $has_termine_permissions;
251        if ($can_edit) {
252            $json_id = json_encode($id);
253            $out .= <<<ZZZZZZZZZZ
254                <div>
255                    <button
256                        id='edit-termin-button'
257                        class='btn btn-primary'
258                        onclick='return olz.editTermin({$json_id})'
259                    >
260                        <img src='{$code_href}assets/icns/edit_white_16.svg' class='noborder' />
261                        Bearbeiten
262                    </button>
263                </div>
264                ZZZZZZZZZZ;
265        }
266
267        // Date & Title
268        $pretty_date = $this->dateUtils()->formatDateTimeRange(
269            $start_date->format('Y-m-d'),
270            $start_time?->format('H:i:s'),
271            $end_date?->format('Y-m-d'),
272            $end_time?->format('H:i:s'),
273            $format = 'long',
274        );
275        $maybe_solv_link = '';
276        if ($solv_uid) {
277            // SOLV-Übersicht-Link zeigen
278            $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";
279        }
280        $label_imgs = implode('', array_map(function (TerminLabel $label) use ($code_path, $code_href) {
281            $ident = $label->getIdent();
282            // TODO: Remove fallback mechanism?
283            $fallback_path = "{$code_path}assets/icns/termine_type_{$ident}_20.svg";
284            $fallback_href = is_file($fallback_path)
285                ? "{$code_href}assets/icns/termine_type_{$ident}_20.svg" : null;
286            $icon_href = $label->getIcon() ? $label->getFileHref($label->getIcon()) : $fallback_href;
287            return $icon_href ? "<img src='{$icon_href}' alt='' class='type-icon'>" : '';
288        }, $labels));
289        $out .= "<h5>{$pretty_date}{$maybe_solv_link}</h5>";
290        $out .= "<h1>{$title} {$label_imgs}</h1>";
291        if ($organizer) {
292            $pretty_organizer = OlzUserInfoModal::render(['user' => $organizer]);
293            $out .= "<div>Organisator: {$pretty_organizer}</div><br>";
294        }
295
296        // Text
297        $text_html = $this->htmlUtils()->renderMarkdown($text, [
298            // TODO: Do NOT ever allow!
299            'html_input' => $start_date->format('Y') > '2020' ? 'escape' : 'allow',
300        ]);
301        $text_html = $termin->replaceImagePaths($text_html);
302        $text_html = $termin->replaceFilePaths($text_html);
303        if ($termin->getDeadline() && $termin->getDeadline() != "0000-00-00") {
304            $text_html .= ($text_html == "" ? "" : "<br />")."Meldeschluss: ".$date_utils->olzDate("t. MM ", $termin->getDeadline());
305        }
306        $out .= "<div>".$text_html."</div>";
307
308        // Link
309        $link = '';
310        if ($solv_uid && $start_date->format('Y-m-d') <= $today && !preg_match('/(Rangliste|Resultat)/', $link)) {
311            // SOLV Ranglisten-Link zeigen
312            $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";
313        }
314        $result_filename = "{$termin_year}-termine-{$id}.xml";
315        if (is_file("{$data_path}results/{$result_filename}")) {
316            // OLZ Ranglisten-Link zeigen
317            $link .= "<div><a href='{$code_href}apps/resultate?file={$result_filename}' target='_blank' class='linkext'>Ranglisten</a></div>\n";
318        } elseif ($can_edit) {
319            // OLZ Rangliste-hochladen-Link zeigen
320            $link .= "<div><a href='{$code_href}apps/resultate?file={$result_filename}' target='_blank' class='linkext'>Rangliste hochladen</a></div>\n";
321        }
322        if ($link == "") {
323            $link = "&nbsp;";
324        } else {
325            $link = str_replace("&", "&amp;", str_replace("&amp;", "&", $link));
326        }
327        $link = str_replace("www.solv.ch", "www.o-l.ch", $link);
328        $out .= "<div class='links'>".$link."</div>";
329
330        // Karte zeigen
331        if ($has_location) {
332            if ($location_name !== null) {
333                $location_maybe_link = $location_name;
334                if ($has_termin_location && $this->authUtils()->hasPermission('termine')) {
335                    $location_maybe_link = "<a href='{$code_href}termine/orte/{$termin_location->getId()}' class='linkmap'>{$location_name}</a>";
336                }
337                $out .= "<h3>Ort: {$location_maybe_link}</h3>";
338            } else {
339                $out .= "<h3>Ort</h3>";
340            }
341            $out .= OlzLocationMap::render([
342                'name' => $location_name ?? '',
343                'latitude' => $lat,
344                'longitude' => $lng,
345                'zoom' => 13,
346            ]);
347            if ($has_termin_location) {
348                $details = $termin_location->getDetails() ?? '';
349                $details_html = $this->htmlUtils()->renderMarkdown($details);
350                $details_html = $termin_location->replaceImagePaths($details_html);
351                $details_html = $termin_location->replaceFilePaths($details_html);
352                $out .= $details_html;
353
354                $location_image_ids = $termin_location->getImageIds();
355                if (count($location_image_ids) > 0) {
356                    $out .= "<div class='lightgallery gallery-container'>";
357                    foreach ($location_image_ids as $image_id) {
358                        $out .= "<div class='gallery-image'>";
359                        $out .= $this->imageUtils()->olzImage(
360                            'termin_locations',
361                            $termin_location->getId(),
362                            $image_id,
363                            128,
364                            'gallery[myset]'
365                        );
366                        $out .= "</div>";
367                    }
368                    $out .= "</div>";
369                }
370            }
371        }
372
373        // Reactions
374        $json_id = json_encode($id);
375        $linked_reactions = $this->htmlUtils()->getLinkedReactions($text_html);
376        $json_reactions = json_encode($linked_reactions) ?: '[]';
377        $out .= "<div id='termin-reactions'></div><script>olz.initTerminReactions({$json_id}{$json_reactions});</script>";
378
379        $out .= "</div>"; // olz-termin-detail
380        $out .= "</div>"; // content-middle
381
382        $out .= OlzFooter::render();
383
384        return $out;
385    }
386
387    protected function getTerminById(int $id): ?Termin {
388        $termin_repo = $this->entityManager()->getRepository(Termin::class);
389        return $termin_repo->findOneBy([
390            'id' => $id,
391            'on_off' => 1,
392        ]);
393    }
394
395    protected function getTimeText(?\DateTime $time): ?string {
396        if (!$time || $time->format('H:i:s') === '00:00:00') {
397            return null;
398        }
399        return $time->format('H:i');
400    }
401}