Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 174
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
OlzAnniversaryParams
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
OlzAnniversary
0.00% covered (danger)
0.00%
0 / 174
0.00% covered (danger)
0.00%
0 / 8
552
0.00% covered (danger)
0.00%
0 / 1
 hasAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 searchSqlWhenHasAccess
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 getRunsHtml
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 1
156
 getElevationStravaHtml
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 getZielsprintHtml
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Olz\Anniversary\Components\OlzAnniversary;
4
5use Doctrine\Common\Collections\Criteria;
6use Olz\Anniversary\Components\OlzAnniversaryRocket\OlzAnniversaryRocket;
7use Olz\Components\Common\OlzEditableText\OlzEditableText;
8use Olz\Components\Common\OlzRootComponent;
9use Olz\Components\OlzZielsprint\OlzZielsprint;
10use Olz\Components\Page\OlzFooter\OlzFooter;
11use Olz\Components\Page\OlzHeader\OlzHeader;
12use Olz\Entity\Anniversary\RunRecord;
13use Olz\Entity\StravaLink;
14use Olz\Repository\Snippets\PredefinedSnippet;
15use Olz\Utils\HttpParams;
16
17/** @extends HttpParams<array{}> */
18class OlzAnniversaryParams extends HttpParams {
19}
20
21/** @extends OlzRootComponent<array<string, mixed>> */
22class OlzAnniversary 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        $snippets_where = $this->searchUtils()->getSnippetsWhereSql([
30            PredefinedSnippet::AnniversaryHoehenmeter,
31            PredefinedSnippet::AnniversaryZielsprint,
32        ], $terms);
33        $static_page_query = $this->searchUtils()->getStaticResultQuery([
34            'link' => "{$code_href}karten",
35            'icon' => "{$code_href}assets/icns/link_map_16.svg",
36            'title' => $this->getPageTitle(),
37            'text' => $this->getPageDescription(),
38        ], $terms);
39        return [
40            'with' => $static_page_query['with'],
41            'query' => <<<ZZZZZZZZZZ
42                    SELECT
43                        '{$code_href}2026' AS link,
44                        '{$code_href}assets/icns/question_mark_20.svg' AS icon,
45                        NULL AS date,
46                        '20 Jahre OL Zimmerberg' AS title,
47                        IFNULL(text, '') AS text,
48                        1.0 AS time_relevance
49                    FROM snippets
50                    WHERE {$snippets_where}
51                UNION ALL
52                    {$static_page_query['query']}
53                ZZZZZZZZZZ,
54        ];
55    }
56
57    public function getPageTitle(): string {
58        return "🎉 20 Jahre OL Zimmerberg 🥳";
59    }
60
61    public function getPageDescription(): string {
62        return "Alle Aktivitäten und Informationen zum Jubiläumsjahr 2026.";
63    }
64
65    public function getHtmlWhenHasAccess(mixed $args): string {
66        $this->httpUtils()->validateGetParams(OlzAnniversaryParams::class);
67
68        $out = OlzHeader::render([
69            'title' => $this->getPageTitle(),
70            'description' => $this->getPageDescription(),
71            'norobots' => true,
72        ]);
73        $out .= <<<ZZZZZZZZZZ
74            <div class='content-full olz-anniversary'>
75                <h1>🎉 20 Jahre OL Zimmerberg 🥳</h1>
76                <br>
77                {$this->getRunsHtml()}
78                {$this->getElevationStravaHtml()}
79                <br>
80                {$this->getZielsprintHtml()}
81            </div>
82            ZZZZZZZZZZ;
83
84        $out .= OlzFooter::render();
85        return $out;
86    }
87
88    protected function getRunsHtml(): string {
89        $code_href = $this->envUtils()->getCodeHref();
90        $user = $this->authUtils()->getCurrentUser();
91        $out = '<h2>🏃 Höhenmeter-Challenge ⛰️</h2>';
92        $out .= OlzEditableText::render(['snippet' => PredefinedSnippet::AnniversaryHoehenmeter]);
93
94        $stats = $this->anniversaryUtils()->getElevationStats();
95        $done_wid = \number_format(max(0, $stats['completion'] * 100), 2);
96        $diff_wid = log10(abs($stats['diffDays']) + 1) * 25;
97        $pretty_sum_meters = number_format($stats['sumMeters'], 0, ".", "'");
98        $pretty_done = number_format($stats['completion'] * 100, 1, ".", "'")."%";
99        $diff_verb = $stats['diffMeters'] >= 0 ? 'sind' : 'liegen';
100        $diff_particle = $stats['diffMeters'] >= 0 ? 'voraus' : 'zurück';
101        $pretty_diff_meters = number_format(abs($stats['diffMeters']), 0, ".", "'")."m";
102        $pretty_diff_days = number_format(abs($stats['diffDays']), 1, ".", "'")." Tage";
103        $rocket = OlzAnniversaryRocket::render();
104        $out .= <<<ZZZZZZZZZZ
105            <div class='elevation-stats'>
106                <div class='done-graph'>
107                    <div class='done-range'></div>
108                    <div class='done-bar' style='width: {$done_wid}%;'></div>
109                    <div
110                        class='rocket test-flaky'
111                        style='left: {$done_wid}%;'
112                        ondblclick='olz.handleRocketClick(this, event)'
113                        ontouchstart='olz.handleRocketTap(this)'
114                    >
115                        {$rocket}
116                    </div>
117                </div>
118                <div>
119                    Wir haben zusammen <b>{$pretty_sum_meters} Höhenmeter</b> bewältigt,
120                    und damit unser Ziel zu <b>{$pretty_done}</b> erreicht.
121                </div>
122                <div class='diff-graph'>
123                    <div class='diff-range'></div>
124                    <div class='diff-bar {$stats['diffKind']}' style='width: {$diff_wid}%;'></div>
125                    <div class='marker' style='left: 12.72%;'></div>
126                    <div class='marker' style='left: 27.42%;'></div>
127                    <div class='marker' style='left: 42.47%;'></div>
128                    <div class='main marker' style='left: 50%;'></div>
129                    <div class='marker' style='left: 57.53%;'></div>
130                    <div class='marker' style='left: 72.58%;'></div>
131                    <div class='marker' style='left: 87.28%;'></div>
132                    <div class='marker-text' style='left: 12.72%;'>-1 Monat</div>
133                    <div class='marker-text' style='left: 27.42%;'>-1 Woche</div>
134                    <div class='marker-text' style='left: 42.47%;'>-1 Tag</div>
135                    <div class='marker-text' style='left: 57.53%;'>+1 Tag</div>
136                    <div class='marker-text' style='left: 72.58%;'>+1 Woche</div>
137                    <div class='marker-text' style='left: 87.28%;'>+1 Monat</div>
138                </div>
139                <div>
140                    Zurzeit {$diff_verb} wir unserem Ziel
141                    <span class='diff-meters {$stats['diffKind']}'>
142                        {$pretty_diff_meters}
143                    </span>
144                    bzw.
145                    <span class='diff-days {$stats['diffKind']}'>
146                        {$pretty_diff_days}
147                    </span>
148                    {$diff_particle}.
149                </div>
150            </div>
151            ZZZZZZZZZZ;
152
153        if (!$user) {
154            $out .= "<p>😕 Du musst <a href='#login-dialog'>eingeloggt</a> sein, um an der Höhenmeter-Challenge teilzunehmen.</p>";
155            return $out;
156        }
157
158        $out .= "<h3>Aktivitäten in den letzten 24 Stunden</h3>";
159        $out .= <<<'ZZZZZZZZZZ'
160            <div class='activities-table activities-24h'>
161                <table>
162                    <tr class='header'>
163                        <td>Datum</td>
164                        <td>Person</td>
165                        <td>Quelle</td>
166                        <td>Distanz</td>
167                        <td>Höhenmeter</td>
168                        <td>Steigung</td>
169                        <td>Art</td>
170                    </tr>
171            ZZZZZZZZZZ;
172        $runs_repo = $this->entityManager()->getRepository(RunRecord::class);
173        $iso_now = $this->dateUtils()->getIsoNow();
174        $minus_one_day = \DateInterval::createFromDateString("-24 hours");
175        $one_day_ago = (new \DateTime($iso_now))->add($minus_one_day);
176        $runs = $runs_repo->matching(Criteria::create()
177            ->where(Criteria::expr()->andX(
178                Criteria::expr()->gt('run_at', $one_day_ago),
179                Criteria::expr()->eq('on_off', 1),
180            ))
181            ->orderBy(['created_at' => 'DESC'])
182            ->setFirstResult(0)
183            ->setMaxResults(1000));
184        foreach ($runs as $run) {
185            $id = $run->getId();
186            $json_id = json_encode($id);
187            $date = $run->getRunAt()->format('d.m.Y H:i');
188            $is_backdated_emoji = $run->getRunAt() < $one_day_ago ? ' 🔙' : '';
189            $is_counting_emoji = $run->getIsCounting() ? '✅' : '🚫';
190            $is_counting_title = $run->getIsCounting() ? 'zählt' : 'zählt nicht';
191            $name = $run->getRunnerName();
192            $source = $this->anniversaryUtils()->getPrettySource($run->getSource() ?? '?');
193            $distance_km = number_format($run->getDistanceMeters() / 1000, 2);
194            $inclination_percent = $run->getDistanceMeters()
195                ? number_format($run->getElevationMeters() * 100 / $run->getDistanceMeters(), 2)
196                : 'NaN';
197            $sport_type = $run->getSportType() ?? "?";
198            $out .= <<<ZZZZZZZZZZ
199                <tr>
200                    <td>{$date}{$is_backdated_emoji}</td>
201                    <td>{$name}</td>
202                    <td>{$source}</td>
203                    <td class='number'>{$distance_km}km</td>
204                    <td class='number'><b>{$run->getElevationMeters()}m</b></td>
205                    <td class='number'>{$inclination_percent}%</td>
206                    <td><span title='{$is_counting_title}'>{$is_counting_emoji} {$sport_type}</span></td>
207                </tr>
208                ZZZZZZZZZZ;
209        }
210        $out .= "</table></div>";
211
212        $out .= <<<ZZZZZZZZZZ
213            <h3>
214                Deine Aktivitäten (ohne Strava)
215                <button
216                    id='create-run-button'
217                    class='btn btn-secondary'
218                    onclick='return olz.initOlzEditRunModal(null, null, {sportType: &quot;Lauf&quot;})'
219                >
220                    <img src='{$code_href}assets/icns/new_white_16.svg' class='noborder' />
221                    Aktivität manuell hinzufügen
222                </button>
223            </h3>
224            ZZZZZZZZZZ;
225        $out .= <<<'ZZZZZZZZZZ'
226            <div class='activities-table activities-manual'>
227                <table>
228                    <tr class='header'>
229                        <td></td>
230                        <td>Datum</td>
231                        <td>Quelle</td>
232                        <td>Distanz</td>
233                        <td>Höhenmeter</td>
234                        <td>Steigung</td>
235                        <td>Art</td>
236                    </tr>
237            ZZZZZZZZZZ;
238        $runs_repo = $this->entityManager()->getRepository(RunRecord::class);
239        $runs = $runs_repo->findBy(
240            ['user' => $user, 'on_off' => 1],
241            ['run_at' => 'DESC'],
242        );
243        foreach ($runs as $run) {
244            $id = $run->getId();
245            $json_id = json_encode($id);
246            $date = $run->getRunAt()->format('d.m.Y H:i:s');
247            $edit_button = $run->getSource() === 'manuell' ? <<<ZZZZZZZZZZ
248                    <button
249                        id='edit-run-{$id}-button'
250                        class='btn btn-secondary-outline btn-sm edit-run-list-button'
251                        onclick='return olz.olzAnniversaryEditRun({$json_id})'
252                    >
253                        <img src='{$code_href}assets/icns/edit_16.svg' class='noborder' />
254                    </button>
255                ZZZZZZZZZZ : '';
256            $source = $this->anniversaryUtils()->getPrettySource($run->getSource() ?? '?');
257            $distance_km = number_format($run->getDistanceMeters() / 1000, 2);
258            $inclination_percent = $run->getDistanceMeters()
259                ? number_format($run->getElevationMeters() * 100 / $run->getDistanceMeters(), 2)
260                : 'NaN';
261            $sport_type = $run->getSportType() ?? '?';
262            $out .= <<<ZZZZZZZZZZ
263                <tr>
264                    <td>{$edit_button}</td>
265                    <td>{$date}</td>
266                    <td>{$source}</td>
267                    <td class='number'>{$distance_km}km</td>
268                    <td class='number'><b>{$run->getElevationMeters()}m</b></td>
269                    <td class='number'>{$inclination_percent}%</td>
270                    <td>{$sport_type}</td>
271                </tr>
272                ZZZZZZZZZZ;
273        }
274        $out .= "</table></div>";
275        return $out;
276    }
277
278    protected function getElevationStravaHtml(): string {
279        $user = $this->authUtils()->getCurrentUser();
280        if (!$user || !$this->authUtils()->hasPermission('anniversary', $user)) {
281            return '';
282        }
283        $strava_link_repo = $this->entityManager()->getRepository(StravaLink::class);
284        $strava_links = $strava_link_repo->findBy(['user' => $user]);
285        $num_strava_links = count($strava_links);
286        $redirect_url = "{$this->envUtils()->getBaseHref()}{$this->envUtils()->getCodeHref()}2026";
287        $strava_url = $this->stravaUtils()->getRegistrationUrl(['read', 'activity:read'], $redirect_url);
288        $out = "<div class='admin-only'><div class='admin-only-text'>Nur für Organisatoren sichtbar</div>";
289        if ($num_strava_links === 0) {
290            $out .= "<p>😕 Kein Strava-Konto verlinkt. <a href='{$strava_url}' class='linkext'>Jetzt mit Strava verbinden!</a></p>";
291        } else {
292            $out .= "<p>✅ Du bist mit diesen {$num_strava_links} Strava-Konten verbunden:</p><ul>";
293            foreach ($strava_links as $strava_link) {
294                $athlete_id = $strava_link->getStravaUser();
295                $athlete_url = "https://www.strava.com/athletes/{$athlete_id}";
296                $date = $strava_link->getLinkedAt()?->format('d.m.Y H:i:s');
297                $out .= "<li><a href='{$athlete_url}'>{$athlete_id}</a> (erstellt: {$date})</li>";
298            }
299            $out .= "</ul>";
300            $out .= "<p><a href='{$strava_url}'>Mit Strava verbinden</a></p>";
301        }
302        $out .= '</div>';
303        return $out;
304    }
305
306    protected function getZielsprintHtml(): string {
307        $out = '<h2>🏁 Zielsprint-Challenge 🏃</h2>';
308        $out .= OlzEditableText::render(['snippet' => PredefinedSnippet::AnniversaryZielsprint]);
309        $out .= OlzZielsprint::render();
310        return $out;
311    }
312}