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