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