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