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