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