1b00cb080SGreg Roach<?php 2b00cb080SGreg Roach 3b00cb080SGreg Roach/** 4b00cb080SGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 6b00cb080SGreg Roach * This program is free software: you can redistribute it and/or modify 7b00cb080SGreg Roach * it under the terms of the GNU General Public License as published by 8b00cb080SGreg Roach * the Free Software Foundation, either version 3 of the License, or 9b00cb080SGreg Roach * (at your option) any later version. 10b00cb080SGreg Roach * This program is distributed in the hope that it will be useful, 11b00cb080SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12b00cb080SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13b00cb080SGreg Roach * GNU General Public License for more details. 14b00cb080SGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16b00cb080SGreg Roach */ 17b00cb080SGreg Roach 18b00cb080SGreg Roachdeclare(strict_types=1); 19b00cb080SGreg Roach 20b00cb080SGreg Roachnamespace Fisharebest\Webtrees\Http\RequestHandlers; 21b00cb080SGreg Roach 22b00cb080SGreg Roachuse Fisharebest\Webtrees\Date; 23b00cb080SGreg Roachuse Fisharebest\Webtrees\Date\FrenchDate; 24b00cb080SGreg Roachuse Fisharebest\Webtrees\Date\GregorianDate; 25b00cb080SGreg Roachuse Fisharebest\Webtrees\Date\HijriDate; 26b00cb080SGreg Roachuse Fisharebest\Webtrees\Date\JalaliDate; 27b00cb080SGreg Roachuse Fisharebest\Webtrees\Date\JewishDate; 28b00cb080SGreg Roachuse Fisharebest\Webtrees\Date\JulianDate; 29b00cb080SGreg Roachuse Fisharebest\Webtrees\Fact; 30b00cb080SGreg Roachuse Fisharebest\Webtrees\Family; 31b00cb080SGreg Roachuse Fisharebest\Webtrees\I18N; 32b00cb080SGreg Roachuse Fisharebest\Webtrees\Individual; 336b9cb339SGreg Roachuse Fisharebest\Webtrees\Registry; 34b00cb080SGreg Roachuse Fisharebest\Webtrees\Services\CalendarService; 35b00cb080SGreg Roachuse Fisharebest\Webtrees\Tree; 36b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 37b00cb080SGreg Roachuse Illuminate\Support\Collection; 38b00cb080SGreg Roachuse Psr\Http\Message\ResponseInterface; 39b00cb080SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 40b00cb080SGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 41b00cb080SGreg Roach 42b00cb080SGreg Roachuse function count; 43b00cb080SGreg Roachuse function e; 44b00cb080SGreg Roachuse function explode; 45b00cb080SGreg Roachuse function get_class; 46b00cb080SGreg Roachuse function ob_get_clean; 47b00cb080SGreg Roachuse function ob_start; 48b00cb080SGreg Roachuse function range; 49b00cb080SGreg Roachuse function response; 50b00cb080SGreg Roachuse function view; 51b00cb080SGreg Roach 52b00cb080SGreg Roach/** 53b00cb080SGreg Roach * Show anniversaries for events in a given day/month/year. 54b00cb080SGreg Roach */ 55b00cb080SGreg Roachclass CalendarEvents implements RequestHandlerInterface 56b00cb080SGreg Roach{ 57*6039542eSGreg Roach public function __construct( 58*6039542eSGreg Roach private readonly CalendarService $calendar_service, 59*6039542eSGreg Roach ) { 60b00cb080SGreg Roach } 61b00cb080SGreg Roach 62b00cb080SGreg Roach /** 63fceda430SGreg Roach * Show anniversaries that occurred on a given day/month/year. 64b00cb080SGreg Roach */ 65b00cb080SGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 66b00cb080SGreg Roach { 67b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 68110d87e5SGreg Roach $view = Validator::attributes($request)->isInArray(['day', 'month', 'year'])->string('view'); 69748dbe15SGreg Roach $cal = Validator::queryParams($request)->string('cal'); 70748dbe15SGreg Roach $day = Validator::queryParams($request)->string('day'); 71748dbe15SGreg Roach $month = Validator::queryParams($request)->string('month'); 72748dbe15SGreg Roach $year = Validator::queryParams($request)->string('year'); 73748dbe15SGreg Roach $filterev = Validator::queryParams($request)->string('filterev'); 74748dbe15SGreg Roach $filterof = Validator::queryParams($request)->string('filterof'); 75748dbe15SGreg Roach $filtersx = Validator::queryParams($request)->string('filtersx'); 76b00cb080SGreg Roach 77b6ac34efSGreg Roach $ged_date = new Date($cal . ' ' . $day . ' ' . $month . ' ' . $year); 78b00cb080SGreg Roach $cal_date = $ged_date->minimumDate(); 79b00cb080SGreg Roach $today = $cal_date->today(); 80b00cb080SGreg Roach 81b00cb080SGreg Roach $days_in_month = $cal_date->daysInMonth(); 82b00cb080SGreg Roach $days_in_week = $cal_date->daysInWeek(); 83b00cb080SGreg Roach 84748dbe15SGreg Roach $CALENDAR_FORMAT = $tree->getPreference('CALENDAR_FORMAT'); 85748dbe15SGreg Roach 86b00cb080SGreg Roach // Day and year share the same layout. 87b00cb080SGreg Roach if ($view !== 'month') { 88b00cb080SGreg Roach if ($view === 'day') { 89b00cb080SGreg Roach $anniversary_facts = $this->calendar_service->getAnniversaryEvents($cal_date->minimumJulianDay(), $filterev, $tree, $filterof, $filtersx); 90b00cb080SGreg Roach } else { 91b00cb080SGreg Roach $ged_year = new Date($cal . ' ' . $year); 92b00cb080SGreg Roach $anniversary_facts = $this->calendar_service->getCalendarEvents($ged_year->minimumJulianDay(), $ged_year->maximumJulianDay(), $filterev, $tree, $filterof, $filtersx); 93b00cb080SGreg Roach } 94b00cb080SGreg Roach 95b00cb080SGreg Roach $anniversaries = Collection::make($anniversary_facts) 96b00cb080SGreg Roach ->unique() 97f25fc0f9SGreg Roach ->sort(static fn (Fact $x, Fact $y): int => $x->date()->minimumJulianDay() <=> $y->date()->minimumJulianDay()); 98b00cb080SGreg Roach 99f25fc0f9SGreg Roach $family_anniversaries = $anniversaries->filter(static fn (Fact $f): bool => $f->record() instanceof Family); 100b00cb080SGreg Roach 101f25fc0f9SGreg Roach $individual_anniversaries = $anniversaries->filter(static fn (Fact $f): bool => $f->record() instanceof Individual); 102b00cb080SGreg Roach 103*6039542eSGreg Roach $family_count = $family_anniversaries 104*6039542eSGreg Roach ->map(static fn (Fact $x): string => $x->record()->xref()) 105*6039542eSGreg Roach ->unique() 106*6039542eSGreg Roach ->count(); 107*6039542eSGreg Roach 108*6039542eSGreg Roach $individual_count = $individual_anniversaries 109*6039542eSGreg Roach ->map(static fn (Fact $x): string => $x->record()->xref()) 110*6039542eSGreg Roach ->unique() 111*6039542eSGreg Roach ->count(); 112*6039542eSGreg Roach 113b00cb080SGreg Roach return response(view('calendar-list', [ 114b00cb080SGreg Roach 'family_anniversaries' => $family_anniversaries, 115b00cb080SGreg Roach 'individual_anniversaries' => $individual_anniversaries, 116*6039542eSGreg Roach 'family_count' => $family_count, 117*6039542eSGreg Roach 'individual_count' => $individual_count, 118b00cb080SGreg Roach ])); 119b00cb080SGreg Roach } 120b00cb080SGreg Roach 121b00cb080SGreg Roach $found_facts = []; 122b00cb080SGreg Roach 123b00cb080SGreg Roach $cal_date->day = 0; 124b00cb080SGreg Roach $cal_date->setJdFromYmd(); 125b00cb080SGreg Roach // Make a separate list for each day. Unspecified/invalid days go in day 0. 126b00cb080SGreg Roach for ($d = 0; $d <= $days_in_month; ++$d) { 127b00cb080SGreg Roach $found_facts[$d] = []; 128b00cb080SGreg Roach } 129b00cb080SGreg Roach // Fetch events for each day 130b00cb080SGreg Roach $jds = range($cal_date->minimumJulianDay(), $cal_date->maximumJulianDay()); 131b00cb080SGreg Roach 132b00cb080SGreg Roach foreach ($jds as $jd) { 133b00cb080SGreg Roach foreach ($this->calendar_service->getAnniversaryEvents($jd, $filterev, $tree, $filterof, $filtersx) as $fact) { 134b00cb080SGreg Roach $tmp = $fact->date()->minimumDate(); 135b00cb080SGreg Roach if ($tmp->day >= 1 && $tmp->day <= $tmp->daysInMonth()) { 136b00cb080SGreg Roach // If the day is valid (for its own calendar), display it in the 137b00cb080SGreg Roach // anniversary day (for the display calendar). 138b00cb080SGreg Roach $found_facts[$jd - $cal_date->minimumJulianDay() + 1][] = $fact; 139b00cb080SGreg Roach } else { 140b00cb080SGreg Roach // Otherwise, display it in the "Day not set" box. 141b00cb080SGreg Roach $found_facts[0][] = $fact; 142b00cb080SGreg Roach } 143b00cb080SGreg Roach } 144b00cb080SGreg Roach } 145b00cb080SGreg Roach 146b00cb080SGreg Roach $cal_facts = []; 147b00cb080SGreg Roach 148b00cb080SGreg Roach foreach ($found_facts as $d => $facts) { 149b00cb080SGreg Roach $cal_facts[$d] = []; 150b00cb080SGreg Roach foreach ($facts as $fact) { 151b00cb080SGreg Roach $xref = $fact->record()->xref(); 15266ecd017SGreg Roach $text = $fact->label() . ' — ' . $fact->date()->display($tree); 153b00cb080SGreg Roach if ($fact->anniv > 0) { 154bfed30e4SGreg Roach $text .= ' (' . I18N::translate('%s year anniversary', I18N::number($fact->anniv)) . ')'; 155b00cb080SGreg Roach } 156b00cb080SGreg Roach if (empty($cal_facts[$d][$xref])) { 157b00cb080SGreg Roach $cal_facts[$d][$xref] = $text; 158b00cb080SGreg Roach } else { 159b00cb080SGreg Roach $cal_facts[$d][$xref] .= '<br>' . $text; 160b00cb080SGreg Roach } 161b00cb080SGreg Roach } 162b00cb080SGreg Roach } 163b00cb080SGreg Roach // We use JD%7 = 0/Mon…6/Sun. Standard definitions use 0/Sun…6/Sat. 164b00cb080SGreg Roach $week_start = (I18N::locale()->territory()->firstDay() + 6) % 7; 165b00cb080SGreg Roach $weekend_start = (I18N::locale()->territory()->weekendStart() + 6) % 7; 166b00cb080SGreg Roach $weekend_end = (I18N::locale()->territory()->weekendEnd() + 6) % 7; 167bfed30e4SGreg Roach 168bfed30e4SGreg Roach // The French calendar has a 10-day week, which starts on primidi. 169b00cb080SGreg Roach if ($days_in_week === 10) { 170b00cb080SGreg Roach $week_start = 0; 171b00cb080SGreg Roach $weekend_start = -1; 172b00cb080SGreg Roach $weekend_end = -1; 173b00cb080SGreg Roach } 174b00cb080SGreg Roach 175b00cb080SGreg Roach ob_start(); 176b00cb080SGreg Roach 177789e127fSGreg Roach echo '<table class="w-100 wt-calendar-month"><thead><tr>'; 178b00cb080SGreg Roach for ($week_day = 0; $week_day < $days_in_week; ++$week_day) { 179b00cb080SGreg Roach $day_name = $cal_date->dayNames(($week_day + $week_start) % $days_in_week); 180b00cb080SGreg Roach if ($week_day === $weekend_start || $week_day === $weekend_end) { 181dd71ff6bSGreg Roach echo '<th class="wt-page-options-label weekend" width="', 100 / $days_in_week, '%">', $day_name, '</th>'; 182b00cb080SGreg Roach } else { 183dd71ff6bSGreg Roach echo '<th class="wt-page-options-label" width="', 100 / $days_in_week, '%">', $day_name, '</th>'; 184b00cb080SGreg Roach } 185b00cb080SGreg Roach } 186b00cb080SGreg Roach echo '</tr>'; 187b00cb080SGreg Roach echo '</thead>'; 188b00cb080SGreg Roach echo '<tbody>'; 189b00cb080SGreg Roach // Print days 1 to n of the month, but extend to cover "empty" days before/after the month to make whole weeks. 190b00cb080SGreg Roach // e.g. instead of 1 -> 30 (=30 days), we might have -1 -> 33 (=35 days) 191b00cb080SGreg Roach $start_d = 1 - ($cal_date->minimumJulianDay() - $week_start) % $days_in_week; 192b00cb080SGreg Roach $end_d = $days_in_month + ($days_in_week - ($cal_date->maximumJulianDay() - $week_start + 1) % $days_in_week) % $days_in_week; 193b00cb080SGreg Roach // Make sure that there is an empty box for any leap/missing days 194b00cb080SGreg Roach if ($start_d === 1 && $end_d === $days_in_month && count($found_facts[0]) > 0) { 195b00cb080SGreg Roach $end_d += $days_in_week; 196b00cb080SGreg Roach } 197b00cb080SGreg Roach for ($d = $start_d; $d <= $end_d; ++$d) { 198b00cb080SGreg Roach if (($d + $cal_date->minimumJulianDay() - $week_start) % $days_in_week === 1) { 199b00cb080SGreg Roach echo '<tr>'; 200b00cb080SGreg Roach } 201b00cb080SGreg Roach echo '<td class="wt-page-options-value">'; 202b00cb080SGreg Roach if ($d < 1 || $d > $days_in_month) { 203b00cb080SGreg Roach if (count($cal_facts[0]) > 0) { 204d8278044SGreg Roach echo '<div class="cal_day">', I18N::translate('Day not set'), '</div>'; 205b00cb080SGreg Roach echo '<div class="small" style="height: 180px; overflow: auto;">'; 2067b6e6159SGreg Roach echo $this->calendarListText($cal_facts[0], $tree); 207b00cb080SGreg Roach echo '</div>'; 208b00cb080SGreg Roach $cal_facts[0] = []; 209b00cb080SGreg Roach } 210b00cb080SGreg Roach } else { 211b00cb080SGreg Roach // Format the day number using the calendar 212e7825935SGreg Roach $tmp = new Date($cal_date->format('%@ ' . $d . ' %O %E')); 213b00cb080SGreg Roach $d_fmt = $tmp->minimumDate()->format('%j'); 214d8278044SGreg Roach echo '<div class="d-flex d-flex justify-content-between">'; 215b00cb080SGreg Roach if ($d === $today->day && $cal_date->month === $today->month) { 216b00cb080SGreg Roach echo '<span class="cal_day current_day">', $d_fmt, '</span>'; 217b00cb080SGreg Roach } else { 218b00cb080SGreg Roach echo '<span class="cal_day">', $d_fmt, '</span>'; 219b00cb080SGreg Roach } 220b00cb080SGreg Roach // Show a converted date 221b00cb080SGreg Roach foreach (explode('_and_', $CALENDAR_FORMAT) as $convcal) { 222*6039542eSGreg Roach $alt_date = match ($convcal) { 223*6039542eSGreg Roach 'french' => new FrenchDate($cal_date->minimumJulianDay() + $d - 1), 224*6039542eSGreg Roach 'gregorian' => new GregorianDate($cal_date->minimumJulianDay() + $d - 1), 225*6039542eSGreg Roach 'jewish' => new JewishDate($cal_date->minimumJulianDay() + $d - 1), 226*6039542eSGreg Roach 'julian' => new JulianDate($cal_date->minimumJulianDay() + $d - 1), 227*6039542eSGreg Roach 'hijri' => new HijriDate($cal_date->minimumJulianDay() + $d - 1), 228*6039542eSGreg Roach 'jalali' => new JalaliDate($cal_date->minimumJulianDay() + $d - 1), 229*6039542eSGreg Roach default => $cal_date, 230*6039542eSGreg Roach }; 231*6039542eSGreg Roach 232b00cb080SGreg Roach if (get_class($alt_date) !== get_class($cal_date) && $alt_date->inValidRange()) { 233b00cb080SGreg Roach echo '<span class="rtl_cal_day">' . $alt_date->format('%j %M') . '</span>'; 234b00cb080SGreg Roach // Just show the first conversion 235b00cb080SGreg Roach break; 236b00cb080SGreg Roach } 237b00cb080SGreg Roach } 238d8278044SGreg Roach echo '</div>'; 239d8278044SGreg Roach echo '<div class="small" style="height: 180px; overflow: auto;">'; 2407b6e6159SGreg Roach echo $this->calendarListText($cal_facts[$d], $tree); 241b00cb080SGreg Roach echo '</div>'; 242b00cb080SGreg Roach } 243b00cb080SGreg Roach echo '</td>'; 244b00cb080SGreg Roach if (($d + $cal_date->minimumJulianDay() - $week_start) % $days_in_week === 0) { 245b00cb080SGreg Roach echo '</tr>'; 246b00cb080SGreg Roach } 247b00cb080SGreg Roach } 248b00cb080SGreg Roach echo '</tbody>'; 249b00cb080SGreg Roach echo '</table>'; 250b00cb080SGreg Roach 251b00cb080SGreg Roach return response(ob_get_clean()); 252b00cb080SGreg Roach } 253b00cb080SGreg Roach 254b00cb080SGreg Roach /** 255b00cb080SGreg Roach * Format a list of facts for display 256b00cb080SGreg Roach * 25709482a55SGreg Roach * @param array<string> $list 258b00cb080SGreg Roach */ 2597b6e6159SGreg Roach private function calendarListText(array $list, Tree $tree): string 260b00cb080SGreg Roach { 261b00cb080SGreg Roach $html = ''; 262b00cb080SGreg Roach 263b00cb080SGreg Roach foreach ($list as $xref => $facts) { 2646b9cb339SGreg Roach $tmp = Registry::gedcomRecordFactory()->make((string) $xref, $tree); 2657b6e6159SGreg Roach $html .= '<a href="' . e($tmp->url()) . '">' . $tmp->fullName() . '</a> '; 2667b6e6159SGreg Roach $html .= '<div class="indent">' . $facts . '</div>'; 267b00cb080SGreg Roach } 268b00cb080SGreg Roach 269b00cb080SGreg Roach return $html; 270b00cb080SGreg Roach } 271b00cb080SGreg Roach} 272