xref: /webtrees/resources/views/lists/individuals-table.phtml (revision eec99f1a5c97e2ce7795a2e8d6b23f86e359534f)
1<?php
2
3declare(strict_types=1);
4
5use Fisharebest\Webtrees\Auth;
6use Fisharebest\Webtrees\Carbon;
7use Fisharebest\Webtrees\Date;
8use Fisharebest\Webtrees\GedcomTag;
9use Fisharebest\Webtrees\I18N;
10use Fisharebest\Webtrees\Module\ModuleChartInterface;
11use Fisharebest\Webtrees\Module\ModuleInterface;
12use Fisharebest\Webtrees\Module\RelationshipsChartModule;
13use Fisharebest\Webtrees\Services\ModuleService;
14use Fisharebest\Webtrees\View;
15use Ramsey\Uuid\Uuid;
16
17// lists requires a unique ID in case there are multiple lists per page
18$table_id = 'table-indi-' . Uuid::uuid4()->toString();
19
20$hundred_years_ago = Carbon::now()->subYears(100)->julianDay();
21
22$unique_indis = []; // Don't double-count indis with multiple names.
23
24$today_jd             = unixtojd();
25$show_estimated_dates = (bool) $tree->getPreference('SHOW_EST_LIST_DATES');
26
27$module = app(ModuleService::class)
28    ->findByComponent(ModuleChartInterface::class, $tree, Auth::user())
29    ->first(static function (ModuleInterface $module) {
30        return $module instanceof RelationshipsChartModule;
31    });
32?>
33
34<?php View::push('javascript') ?>
35<script>
36$("#<?= e($table_id) ?> > .wt-table-individual").dataTable({
37    processing: true,
38    retrieve: true,
39    columns: [
40        /* Given names  */ { type: "text" },
41        /* Surnames     */ { type: "text" },
42        /* SOSA number  */ { type: "num", visible: <?= json_encode($sosa) ?> },
43        /* Birth date   */ { type: "num" },
44        /* Anniversary  */ { type: "num" },
45        /* Birthplace   */ { type: "text" },
46        /* Children     */ { type: "num" },
47        /* Deate date   */ { type: "num" },
48        /* Anniversary  */ { type: "num" },
49        /* Age          */ { type: "num" },
50        /* Death place  */ { type: "text" },
51        /* Last change  */ { visible: <?= json_encode($tree->getPreference('SHOW_LAST_CHANGE')) ?> },
52        /* Filter sex   */ { sortable: false },
53        /* Filter birth */ { sortable: false },
54        /* Filter death */ { sortable: false },
55        /* Filter tree  */ { sortable: false }
56    ],
57    sorting: <?= json_encode($sosa ? [[4, 'asc']] : [[1, 'asc']]) ?>
58});
59
60$("#<?= e($table_id) ?>")
61    /* Hide/show parents */
62    .on("click", "#btn-toggle-parents", function() {
63        $(".wt-individual-list-parents").slideToggle();
64    })
65    /* Hide/show statistics */
66    .on("click", "#btn-toggle-statistics", function() {
67        $("#individual-charts-<?= e($table_id) ?>").slideToggle({
68            complete: function () {
69                // Trigger resize to redraw the chart
70                $('div[id^="google-chart-"]').resize();
71            }
72        });
73    })
74    /* Filter buttons in table header */
75    .on("click", "input[data-filter-column]", function() {
76        let checkbox = $(this);
77        let siblings = checkbox.parent().siblings();
78
79        // Deselect other options
80        siblings.children().prop("checked", false).removeAttr("checked");
81        siblings.removeClass('active');
82
83        // Apply (or clear) this filter
84        let checked = checkbox.prop("checked");
85        let filter  = checked ? checkbox.data("filter-value") : "";
86        let column  = $("#<?= e($table_id) ?> .wt-table-individual").DataTable().column(checkbox.data("filter-column"));
87        column.search(filter).draw();
88    });
89</script>
90<?php View::endpush() ?>
91
92<?php
93$max_age = (int) $tree->getPreference('MAX_ALIVE_AGE');
94
95// Inititialise chart data
96$deat_by_age = [];
97for ($age = 0; $age <= $max_age; $age++) {
98    $deat_by_age[$age]['M'] = 0;
99    $deat_by_age[$age]['F'] = 0;
100    $deat_by_age[$age]['U'] = 0;
101}
102$birt_by_decade = [];
103$deat_by_decade = [];
104for ($year = 1400; $year < 2050; $year += 10) {
105    $birt_by_decade[$year]['M'] = 0;
106    $birt_by_decade[$year]['F'] = 0;
107    $birt_by_decade[$year]['U'] = 0;
108    $deat_by_decade[$year]['M'] = 0;
109    $deat_by_decade[$year]['F'] = 0;
110    $deat_by_decade[$year]['U'] = 0;
111}
112
113$birthData = [
114    [
115        [
116            'label' => I18N::translate('Century'),
117            'type'  => 'date',
118        ], [
119            'label' => I18N::translate('Males'),
120            'type'  => 'number',
121        ], [
122            'label' => I18N::translate('Females'),
123            'type'  => 'number',
124        ],
125    ]
126];
127
128$deathData = [
129    [
130        [
131            'label' => I18N::translate('Century'),
132            'type'  => 'date',
133        ], [
134            'label' => I18N::translate('Males'),
135            'type'  => 'number',
136        ], [
137            'label' => I18N::translate('Females'),
138            'type'  => 'number',
139        ],
140    ]
141];
142
143$deathAgeData = [
144    [
145        I18N::translate('Age'),
146        I18N::translate('Males'),
147        I18N::translate('Females'),
148        I18N::translate('Average age'),
149    ]
150];
151
152?>
153
154<div id="<?= e($table_id) ?>">
155    <table class="table table-bordered table-sm wt-table-individual"
156        <?= view('lists/datatables-attributes') ?>
157    >
158        <thead>
159            <tr>
160                <th colspan="16">
161                    <div class="btn-toolbar d-flex justify-content-between mb-2" role="toolbar">
162                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
163                            <label class="btn btn-secondary" title="<?= I18N::translate('Show only males.') ?>">
164                                <input type="checkbox" data-filter-column="12" data-filter-value="M">
165                                <?= view('icons/sex', ['sex' => 'M']) ?>
166                            </label>
167                            <label class="btn btn-secondary" title="<?= I18N::translate('Show only females.') ?>">
168                                <input type="checkbox" data-filter-column="12" data-filter-value="F">
169                                <?= view('icons/sex', ['sex' => 'F']) ?>
170                            </label>
171                            <label class="btn btn-secondary" title="<?= I18N::translate('Show only individuals for whom the gender is not known.') ?>">
172                                <input type="checkbox" data-filter-column="12" data-filter-value="U">
173                                <?= view('icons/sex', ['sex' => 'U']) ?>
174                            </label>
175                        </div>
176
177                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
178                            <label class="btn btn-secondary" title="<?= I18N::translate('Show individuals who are alive or couples where both partners are alive.') ?>">
179                                <input type="checkbox" data-filter-column="14" data-filter-value="N">
180                                <?= I18N::translate('Alive') ?>
181                            </label>
182                            <label class="btn btn-secondary" title="<?= I18N::translate('Show individuals who are dead or couples where both partners are dead.') ?>">
183                                <input type="checkbox" data-filter-column="14" data-filter-value="Y">
184                                <?= I18N::translate('Dead') ?>
185                            </label>
186                            <label class="btn btn-secondary" title="<?= I18N::translate('Show individuals who died more than 100 years ago.') ?>">
187                                <input type="checkbox" data-filter-column="14" data-filter-value="YES">
188                                <?= I18N::translate('Death') ?>&gt;100
189                            </label>
190                            <label class="btn btn-secondary" title="<?= I18N::translate('Show individuals who died within the last 100 years.') ?>">
191                                <input type="checkbox" data-filter-column="14" data-filter-value="Y100">
192                                <?= I18N::translate('Death') ?>&lt;=100
193                            </label>
194                        </div>
195
196                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
197                            <label class="btn btn-secondary" title="<?= I18N::translate('Show individuals born more than 100 years ago.') ?>">
198                                <input type="checkbox" data-filter-column="13" data-filter-value="YES">
199                                <?= I18N::translate('Birth') ?>&gt;100
200                            </label>
201                            <label class="btn btn-secondary" title="<?= I18N::translate('Show individuals born within the last 100 years.') ?>">
202                                <input type="checkbox" data-filter-column="13" data-filter-value="Y100">
203                                <?= I18N::translate('Birth') ?>&lt;=100
204                            </label>
205                        </div>
206
207                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
208                            <label class="btn btn-secondary" title="<?= I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') ?>">
209                                <input type="checkbox" data-filter-column="15" data-filter-value="R">
210                                <?= I18N::translate('Roots') ?>
211                            </label>
212                            <label class="btn btn-secondary" title="<?= I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') ?>">
213                                <input type="checkbox" data-filter-column="15" data-filter-value="L">
214                                <?= I18N::translate('Leaves') ?>
215                            </label>
216                        </div>
217                    </div>
218                </th>
219            </tr>
220            <tr>
221                <th><?= I18N::translate('Given names') ?></th>
222                <th><?= I18N::translate('Surname') ?></th>
223                <th><?= /* I18N: Abbreviation for “Sosa-Stradonitz number”. This is an individual’s surname, so may need transliterating into non-latin alphabets. */
224                    I18N::translate('Sosa') ?></th>
225                <th><?= I18N::translate('Birth') ?></th>
226                <th>
227                    <span title="<?= I18N::translate('Anniversary') ?>">
228                        <?= view('icons/anniversary') ?>
229                    </span>
230                </th>
231                <th><?= I18N::translate('Place') ?></th>
232                <th>
233                    <i class="icon-children" title="<?= I18N::translate('Children') ?>"></i>
234                </th>
235                <th><?= I18N::translate('Death') ?></th>
236                <th>
237                    <span title="<?= I18N::translate('Anniversary') ?>">
238                        <?= view('icons/anniversary') ?>
239                    </span>
240                </th>
241                <th><?= I18N::translate('Age') ?></th>
242                <th><?= I18N::translate('Place') ?></th>
243                <th><?= I18N::translate('Last change') ?></th>
244                <th hidden></th>
245                <th hidden></th>
246                <th hidden></th>
247                <th hidden></th>
248            </tr>
249        </thead>
250
251        <tbody>
252            <?php foreach ($individuals as $key => $individual) : ?>
253            <tr class="<?= $individual->isPendingDeletion() ? 'wt-old' : ($individual->isPendingAddition() ? 'wt-new' : '') ?>">
254                <td colspan="2" data-sort="<?= e(str_replace([',', '@P.N.', '@N.N.'], 'AAAA', implode(',', array_reverse(explode(',', $individual->sortName()))))) ?>">
255                    <?php foreach ($individual->getAllNames() as $num => $name) : ?>
256                        <a title="<?= $name['type'] === 'NAME' ? '' : strip_tags(GedcomTag::getLabel($name['type'], $individual)) ?>" href="<?= e($individual->url()) ?>" class="<?= $num === $individual->getPrimaryName() ? 'name2' : '' ?>">
257                            <?= $name['full'] ?>
258                        </a>
259                        <?php if ($num === $individual->getPrimaryName()) : ?>
260                            <small><?= view('icons/sex', ['sex' => $individual->sex()]) ?></small>
261                        <?php endif ?>
262                        <br>
263                    <?php endforeach ?>
264                    <?= view('lists/individual-table-parents', ['individual' => $individual]) ?>
265                </td>
266
267                <td hidden data-sort="<?= e(str_replace([',', '@P.N.', '@N.N.'], 'AAAA', $individual->sortName())) ?>"></td>
268
269                <td class="text-center" data-sort="<?= $key ?>">
270                    <?php if ($sosa) : ?>
271                        <?php if ($module instanceof RelationshipsChartModule) : ?>
272                            <a href="<?= e($module->chartUrl($individuals[1], ['xref2' => $individual->xref()])) ?>" rel="nofollow" title="<?= I18N::translate('Relationships') ?>" rel="nofollow">
273                                <?= I18N::number($key) ?>
274                            </a>
275                        <?php else : ?>
276                            <?= I18N::number($key) ?>
277                        <?php endif ?>
278                    <?php endif ?>
279                </td>
280
281                <!-- Birth date -->
282                <td data-sort="<?= $individual->getEstimatedBirthDate()->julianDay() ?>">
283                    <?php $birth_dates = $individual->getAllBirthDates(); ?>
284
285                    <?php foreach ($birth_dates as $n => $birth_date) : ?>
286                        <?= $birth_date->display(true) ?>
287                        <br>
288                    <?php endforeach ?>
289
290                    <?php if (empty($birth_dates) && $show_estimated_dates) : ?>
291                        <?= $individual->getEstimatedBirthDate()->display(true) ?>
292                    <?php endif ?>
293                </td>
294
295                <!-- Birth anniversary -->
296                <td class="text-center" data-sort="<?= - $individual->getEstimatedBirthDate()->julianDay() ?>">
297                    <?php if (isset($birth_dates[0]) && $birth_dates[0]->gregorianYear() >= 1550 && $birth_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->xref()])) : ?>
298                        <?php
299                            ++$birt_by_decade[(int) ($birth_dates[0]->gregorianYear() / 10) * 10][$individual->sex()];
300                        ?>
301                        <?= Date::getAge($birth_dates[0]) ?>
302                    <?php endif ?>
303                </td>
304
305                <!-- Birth place -->
306                <td>
307                    <?php foreach ($individual->getAllBirthPlaces() as $n => $birth_place) : ?>
308                        <?= $birth_place->shortName(true) ?>
309                        <br>
310                    <?php endforeach ?>
311                </td>
312
313                <!-- Number of children -->
314                <td class="text-center" data-sort="<?= $individual->numberOfChildren() ?>">
315                    <?= I18N::number($individual->numberOfChildren()) ?>
316                </td>
317
318                <!--    Death date -->
319                <?php $death_dates = $individual->getAllDeathDates() ?>
320                <td data-sort="<?= $individual->getEstimatedDeathDate()->julianDay() ?>">
321                    <?php foreach ($death_dates as $num => $death_date) : ?>
322                        <?= $death_date->display(true) ?>
323                    <br>
324                    <?php endforeach ?>
325
326                    <?php if (empty($death_dates) && $show_estimated_dates && $individual->getEstimatedDeathDate()->minimumDate()->minimumJulianDay() < $today_jd) : ?>
327                        <?= $individual->getEstimatedDeathDate()->display(true) ?>
328                    <?php endif ?>
329                </td>
330
331                <!-- Death anniversary -->
332                <td class="text-center" data-sort="<?= - $individual->getEstimatedDeathDate()->julianDay() ?>">
333                    <?php if (isset($death_dates[0]) && $death_dates[0]->gregorianYear() >= 1550 && $death_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->xref()])) : ?>
334                        <?php
335                            ++$deat_by_decade[(int) ($death_dates[0]->gregorianYear() / 10) * 10][$individual->sex()];
336                        ?>
337                        <?= Date::getAge($death_dates[0]) ?>
338                    <?php endif ?>
339                </td>
340
341                <!-- Age at death -->
342                <?php if (isset($birth_dates[0], $death_dates[0])) : ?>
343                    <?php $age_at_death_years = Date::getAgeYears($birth_dates[0], $death_dates[0]); ?>
344                    <?php $age_at_death_sort = Date::getAge($birth_dates[0], $death_dates[0]); ?>
345                    <?php $age_at_death_display = I18N::number($age_at_death_years); ?>
346                    <?php if (!isset($unique_indis[$individual->xref()]) && $age_at_death_years >= 0 && $age_at_death_years <= $max_age) : ?>
347                        <?php
348                            ++$deat_by_age[$age_at_death_years][$individual->sex()];
349                        ?>
350                    <?php endif ?>
351                <?php else : ?>
352                    <?php $age_at_death_display = ''; ?>
353                    <?php $age_at_death_sort = PHP_INT_MAX; ?>
354                <?php endif ?>
355                <td class="text-center" data-sort="<?= e($age_at_death_sort) ?>">
356                    <?= e($age_at_death_display) ?>
357                </td>
358
359                <!-- Death place -->
360                <td>
361                    <?php foreach ($individual->getAllDeathPlaces() as $n => $death_place) : ?>
362                        <?= $death_place->shortName(true) ?>
363                        <br>
364                    <?php endforeach ?>
365                </td>
366
367                <!-- Last change -->
368                <td data-sort="<?= $individual->lastChangeTimestamp()->unix() ?>">
369                    <?= view('components/datetime', ['timestamp' => $individual->lastChangeTimestamp()]) ?>
370                </td>
371
372                <!-- Filter by sex -->
373                <td hidden>
374                    <?= $individual->sex() ?>
375                </td>
376
377                <!-- Filter by birth date -->
378                <td hidden>
379                    <?php if (!$individual->canShow() || $individual->getEstimatedBirthDate()->maximumJulianDay() > $hundred_years_ago) : ?>
380                        Y100
381                    <?php else : ?>
382                        YES
383                    <?php endif ?>
384                </td>
385
386                <!-- Filter by death date -->
387                <td hidden>
388                    <?php if ($individual->getEstimatedDeathDate()->maximumJulianDay() > $hundred_years_ago) : ?>
389                        Y100
390                    <?php elseif ($individual->isDead()) : ?>
391                        YES
392                    <?php else : ?>
393                        N
394                    <?php endif ?>
395                </td>
396
397                <!-- Filter by roots/leaves -->
398                <td hidden>
399                    <?php if ($individual->childFamilies()->isEmpty()) : ?>
400                        R
401                    <?php elseif (!$individual->isDead() && $individual->numberOfChildren() < 1) : ?>
402                        L
403                    <?php endif ?>
404                </td>
405            </tr>
406
407                <?php $unique_indis[$individual->xref()] = true ?>
408            <?php endforeach ?>
409        </tbody>
410
411        <tfoot>
412            <tr>
413                <th colspan="16">
414                    <div class="btn-group btn-group-sm">
415                        <button id="btn-toggle-parents" class="btn btn-secondary" data-toggle="button" data-persist="show-parents">
416                            <?= I18N::translate('Show parents') ?>
417                        </button>
418                        <button id="btn-toggle-statistics" class="btn btn-secondary" data-toggle="button" data-persist="show-statistics">
419                            <?= I18N::translate('Show statistics charts') ?>
420                        </button>
421                    </div>
422                </th>
423            </tr>
424        </tfoot>
425    </table>
426</div>
427
428<div id="individual-charts-<?= e($table_id) ?>" style="display: none;">
429    <div class="mb-3">
430        <div class="card-deck">
431            <div class="col-lg-12 col-md-12 mb-3">
432                <div class="card m-0">
433                    <div class="card-header">
434                        <?= I18N::translate('Decade of birth') ?>
435                    </div>
436                    <div class="card-body">
437                        <?php
438                        foreach ($birt_by_decade as $century => $values) {
439                            if (($values['M'] + $values['F']) > 0) {
440                                $birthData[] = [
441                                    [
442                                        'v' => 'Date(' . $century . ', 0, 1)',
443                                        'f' => $century,
444                                    ],
445                                    $values['M'],
446                                    $values['F'],
447                                ];
448                            }
449                        }
450                        ?>
451                        <?= view('lists/chart-by-decade', ['data' => $birthData, 'title' => I18N::translate('Decade of birth')]) ?>
452                    </div>
453                </div>
454            </div>
455        </div>
456        <div class="card-deck">
457            <div class="col-lg-12 col-md-12 mb-3">
458                <div class="card m-0">
459                    <div class="card-header">
460                        <?= I18N::translate('Decade of death') ?>
461                    </div>
462                    <div class="card-body">
463                        <?php
464                        foreach ($deat_by_decade as $century => $values) {
465                            if (($values['M'] + $values['F']) > 0) {
466                                $deathData[] = [
467                                    [
468                                        'v' => 'Date(' . $century . ', 0, 1)',
469                                        'f' => $century,
470                                    ],
471                                    $values['M'],
472                                    $values['F'],
473                                ];
474                            }
475                        }
476                        ?>
477                        <?= view('lists/chart-by-decade', ['data' => $deathData, 'title' => I18N::translate('Decade of death')]) ?>
478                    </div>
479                </div>
480            </div>
481        </div>
482        <div class="card-deck">
483            <div class="col-lg-12 col-md-12 mb-3">
484                <div class="card m-0">
485                    <div class="card-header">
486                        <?= I18N::translate('Age related to death year') ?>
487                    </div>
488                    <div class="card-body">
489                        <?php
490                            $totalAge = 0;
491                            $totalSum = 0;
492                            $max      = 0;
493
494                        foreach ($deat_by_age as $age => $values) {
495                            if (($values['M'] + $values['F']) > 0) {
496                                if (($values['M'] + $values['F']) > $max) {
497                                    $max = $values['M'] + $values['F'];
498                                }
499
500                                $totalAge += $age * ($values['M'] + $values['F']);
501                                $totalSum += $values['M'] + $values['F'];
502
503                                $deathAgeData[] = [
504                                    $age,
505                                    $values['M'],
506                                    $values['F'],
507                                    null,
508                                ];
509                            }
510                        }
511
512                        if ($totalSum > 0) {
513                            $deathAgeData[] = [
514                                round($totalAge / $totalSum, 1),
515                                null,
516                                null,
517                                0,
518                            ];
519
520                            $deathAgeData[] = [
521                                round($totalAge / $totalSum, 1),
522                                null,
523                                null,
524                                $max,
525                            ];
526                        }
527                        ?>
528                        <?= view('lists/chart-by-age', ['data' => $deathAgeData, 'title' => I18N::translate('Age related to death year')]) ?>
529                    </div>
530                </div>
531            </div>
532        </div>
533    </div>
534</div>
535