1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees\Module; 20 21use Fisharebest\Webtrees\Carbon; 22use Fisharebest\Webtrees\Gedcom; 23use Fisharebest\Webtrees\GedcomTag; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Services\CalendarService; 26use Fisharebest\Webtrees\Tree; 27use Illuminate\Support\Str; 28use Psr\Http\Message\ServerRequestInterface; 29 30/** 31 * Class UpcomingAnniversariesModule 32 */ 33class UpcomingAnniversariesModule extends AbstractModule implements ModuleBlockInterface 34{ 35 use ModuleBlockTrait; 36 37 // Default values for new blocks. 38 private const DEFAULT_DAYS = '7'; 39 private const DEFAULT_FILTER = '1'; 40 private const DEFAULT_SORT = 'alpha'; 41 private const DEFAULT_STYLE = 'table'; 42 43 // Can show this number of days into the future. 44 private const MIN_DAYS = 1; 45 private const MAX_DAYS = 30; 46 47 // All standard GEDCOM 5.5.1 events except CENS, RESI and EVEN 48 private const ALL_EVENTS = [ 49 'ADOP', 50 'ANUL', 51 'BAPM', 52 'BARM', 53 'BASM', 54 'BIRT', 55 'BLES', 56 'BURI', 57 'CHR', 58 'CHRA', 59 'CONF', 60 'CREM', 61 'DEAT', 62 'DIV', 63 'DIVF', 64 'EMIG', 65 'ENGA', 66 'FCOM', 67 'GRAD', 68 'IMMI', 69 'MARB', 70 'MARC', 71 'MARL', 72 'MARR', 73 'MARS', 74 'NATU', 75 'ORDN', 76 'PROB', 77 'RETI', 78 'WILL', 79 ]; 80 81 private const DEFAULT_EVENTS = [ 82 'BIRT', 83 'MARR', 84 'DEAT', 85 ]; 86 87 /** 88 * How should this module be identified in the control panel, etc.? 89 * 90 * @return string 91 */ 92 public function title(): string 93 { 94 /* I18N: Name of a module */ 95 return I18N::translate('Upcoming events'); 96 } 97 98 /** 99 * A sentence describing what this module does. 100 * 101 * @return string 102 */ 103 public function description(): string 104 { 105 /* I18N: Description of the “Upcoming events” module */ 106 return I18N::translate('A list of the anniversaries that will occur in the near future.'); 107 } 108 109 /** 110 * Generate the HTML content of this block. 111 * 112 * @param Tree $tree 113 * @param int $block_id 114 * @param string $context 115 * @param string[] $config 116 * 117 * @return string 118 */ 119 public function getBlock(Tree $tree, int $block_id, string $context, array $config = []): string 120 { 121 $calendar_service = new CalendarService(); 122 123 $default_events = implode(',', self::DEFAULT_EVENTS); 124 125 $days = (int) $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS); 126 $filter = (bool) $this->getBlockSetting($block_id, 'filter', self::DEFAULT_FILTER); 127 $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_STYLE); 128 $sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT); 129 $events = $this->getBlockSetting($block_id, 'events', $default_events); 130 131 extract($config, EXTR_OVERWRITE); 132 133 $event_array = explode(',', $events); 134 135 // If we are only showing living individuals, then we don't need to search for DEAT events. 136 if ($filter) { 137 $event_array = array_diff($event_array, Gedcom::DEATH_EVENTS); 138 } 139 140 $events_filter = implode('|', $event_array); 141 142 $startjd = Carbon::now()->julianDay() + 1; 143 $endjd = Carbon::now()->julianDay() + $days; 144 145 $facts = $calendar_service->getEventsList($startjd, $endjd, $events_filter, $filter, $sortStyle, $tree); 146 147 if (empty($facts)) { 148 if ($endjd == $startjd) { 149 $content = view('modules/upcoming_events/empty', [ 150 'message' => I18N::translate('No events exist for tomorrow.'), 151 ]); 152 } else { 153 /* I18N: translation for %s==1 is unused; it is translated separately as “tomorrow” */ $content = view('modules/upcoming_events/empty', [ 154 'message' => I18N::plural('No events exist for the next %s day.', 'No events exist for the next %s days.', $endjd - $startjd + 1, I18N::number($endjd - $startjd + 1)), 155 ]); 156 } 157 } elseif ($infoStyle === 'list') { 158 $content = view('lists/anniversaries-list', [ 159 'facts' => $facts, 160 ]); 161 } else { 162 $content = view('lists/anniversaries-table', [ 163 'facts' => $facts, 164 ]); 165 } 166 167 if ($context !== self::CONTEXT_EMBED) { 168 return view('modules/block-template', [ 169 'block' => Str::kebab($this->name()), 170 'id' => $block_id, 171 'config_url' => $this->configUrl($tree, $context, $block_id), 172 'title' => $this->title(), 173 'content' => $content, 174 ]); 175 } 176 177 return $content; 178 } 179 180 /** 181 * Should this block load asynchronously using AJAX? 182 * 183 * Simple blocks are faster in-line, more complex ones can be loaded later. 184 * 185 * @return bool 186 */ 187 public function loadAjax(): bool 188 { 189 return true; 190 } 191 192 /** 193 * Can this block be shown on the user’s home page? 194 * 195 * @return bool 196 */ 197 public function isUserBlock(): bool 198 { 199 return true; 200 } 201 202 /** 203 * Can this block be shown on the tree’s home page? 204 * 205 * @return bool 206 */ 207 public function isTreeBlock(): bool 208 { 209 return true; 210 } 211 212 /** 213 * Update the configuration for a block. 214 * 215 * @param ServerRequestInterface $request 216 * @param int $block_id 217 * 218 * @return void 219 */ 220 public function saveBlockConfiguration(ServerRequestInterface $request, int $block_id): void 221 { 222 $params = $request->getParsedBody(); 223 224 $this->setBlockSetting($block_id, 'days', $params['days']); 225 $this->setBlockSetting($block_id, 'filter', $params['filter']); 226 $this->setBlockSetting($block_id, 'infoStyle', $params['infoStyle']); 227 $this->setBlockSetting($block_id, 'sortStyle', $params['sortStyle']); 228 $this->setBlockSetting($block_id, 'events', implode(',', $params['events'] ?? [])); 229 } 230 231 /** 232 * An HTML form to edit block settings 233 * 234 * @param Tree $tree 235 * @param int $block_id 236 * 237 * @return string 238 */ 239 public function editBlockConfiguration(Tree $tree, int $block_id): string 240 { 241 $default_events = implode(',', self::DEFAULT_EVENTS); 242 243 $days = (int) $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS); 244 $filter = $this->getBlockSetting($block_id, 'filter', self::DEFAULT_FILTER); 245 $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_STYLE); 246 $sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT); 247 $events = $this->getBlockSetting($block_id, 'events', $default_events); 248 249 $event_array = explode(',', $events); 250 251 $all_events = []; 252 foreach (self::ALL_EVENTS as $event) { 253 $all_events[$event] = GedcomTag::getLabel($event); 254 } 255 256 $info_styles = [ 257 /* I18N: An option in a list-box */ 258 'list' => I18N::translate('list'), 259 /* I18N: An option in a list-box */ 260 'table' => I18N::translate('table'), 261 ]; 262 263 $sort_styles = [ 264 /* I18N: An option in a list-box */ 265 'alpha' => I18N::translate('sort by name'), 266 /* I18N: An option in a list-box */ 267 'anniv' => I18N::translate('sort by date'), 268 ]; 269 270 return view('modules/upcoming_events/config', [ 271 'all_events' => $all_events, 272 'days' => $days, 273 'event_array' => $event_array, 274 'filter' => $filter, 275 'infoStyle' => $infoStyle, 276 'info_styles' => $info_styles, 277 'max_days' => self::MAX_DAYS, 278 'sortStyle' => $sortStyle, 279 'sort_styles' => $sort_styles, 280 ]); 281 } 282} 283