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