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