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