xref: /webtrees/app/Http/RequestHandlers/IndividualPage.php (revision a091ac74647eab281b25090b737835eeea14ae10)
1852ede8cSGreg Roach<?php
2852ede8cSGreg Roach
3852ede8cSGreg Roach/**
4852ede8cSGreg Roach * webtrees: online genealogy
5*a091ac74SGreg Roach * Copyright (C) 2020 webtrees development team
6852ede8cSGreg Roach * This program is free software: you can redistribute it and/or modify
7852ede8cSGreg Roach * it under the terms of the GNU General Public License as published by
8852ede8cSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9852ede8cSGreg Roach * (at your option) any later version.
10852ede8cSGreg Roach * This program is distributed in the hope that it will be useful,
11852ede8cSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12852ede8cSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13852ede8cSGreg Roach * GNU General Public License for more details.
14852ede8cSGreg Roach * You should have received a copy of the GNU General Public License
15852ede8cSGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
16852ede8cSGreg Roach */
17852ede8cSGreg Roach
18852ede8cSGreg Roachdeclare(strict_types=1);
19852ede8cSGreg Roach
20852ede8cSGreg Roachnamespace Fisharebest\Webtrees\Http\RequestHandlers;
21852ede8cSGreg Roach
22e5d858f5SGreg Roachuse Fig\Http\Message\StatusCodeInterface;
23054771e9SGreg Roachuse Fisharebest\Webtrees\Age;
24852ede8cSGreg Roachuse Fisharebest\Webtrees\Auth;
25852ede8cSGreg Roachuse Fisharebest\Webtrees\Date;
26852ede8cSGreg Roachuse Fisharebest\Webtrees\Fact;
27*a091ac74SGreg Roachuse Fisharebest\Webtrees\Factory;
28852ede8cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsPrint;
29852ede8cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
30852ede8cSGreg Roachuse Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
31852ede8cSGreg Roachuse Fisharebest\Webtrees\GedcomTag;
32852ede8cSGreg Roachuse Fisharebest\Webtrees\Http\ViewResponseTrait;
33852ede8cSGreg Roachuse Fisharebest\Webtrees\I18N;
34852ede8cSGreg Roachuse Fisharebest\Webtrees\Individual;
35852ede8cSGreg Roachuse Fisharebest\Webtrees\Media;
36852ede8cSGreg Roachuse Fisharebest\Webtrees\MediaFile;
37852ede8cSGreg Roachuse Fisharebest\Webtrees\Module\ModuleSidebarInterface;
38852ede8cSGreg Roachuse Fisharebest\Webtrees\Module\ModuleTabInterface;
39852ede8cSGreg Roachuse Fisharebest\Webtrees\Services\ClipboardService;
40852ede8cSGreg Roachuse Fisharebest\Webtrees\Services\ModuleService;
41852ede8cSGreg Roachuse Fisharebest\Webtrees\Services\UserService;
42852ede8cSGreg Roachuse Fisharebest\Webtrees\Tree;
43852ede8cSGreg Roachuse Illuminate\Support\Collection;
44852ede8cSGreg Roachuse Psr\Http\Message\ResponseInterface;
45852ede8cSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
46852ede8cSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
47852ede8cSGreg Roachuse stdClass;
48852ede8cSGreg Roach
492406e0e0SGreg Roachuse function array_map;
50852ede8cSGreg Roachuse function assert;
512406e0e0SGreg Roachuse function date;
52852ede8cSGreg Roachuse function e;
53852ede8cSGreg Roachuse function explode;
542406e0e0SGreg Roachuse function implode;
55852ede8cSGreg Roachuse function is_string;
56852ede8cSGreg Roachuse function ob_get_clean;
57852ede8cSGreg Roachuse function ob_start;
58852ede8cSGreg Roachuse function preg_match_all;
59852ede8cSGreg Roachuse function preg_replace;
60852ede8cSGreg Roachuse function redirect;
61852ede8cSGreg Roachuse function route;
62852ede8cSGreg Roachuse function str_replace;
63852ede8cSGreg Roachuse function strpos;
642406e0e0SGreg Roachuse function strtoupper;
652406e0e0SGreg Roachuse function view;
662406e0e0SGreg Roach
672406e0e0SGreg Roachuse const PREG_SET_ORDER;
68852ede8cSGreg Roach
69852ede8cSGreg Roach/**
70852ede8cSGreg Roach * Show an individual's page.
71852ede8cSGreg Roach */
72852ede8cSGreg Roachclass IndividualPage implements RequestHandlerInterface
73852ede8cSGreg Roach{
74852ede8cSGreg Roach    use ViewResponseTrait;
75852ede8cSGreg Roach
76852ede8cSGreg Roach    /** @var ClipboardService */
77852ede8cSGreg Roach    private $clipboard_service;
78852ede8cSGreg Roach
79852ede8cSGreg Roach    /** @var ModuleService */
80852ede8cSGreg Roach    private $module_service;
81852ede8cSGreg Roach
82852ede8cSGreg Roach    /** @var UserService */
83852ede8cSGreg Roach    private $user_service;
84852ede8cSGreg Roach
85852ede8cSGreg Roach    /**
86aba707cdSGreg Roach     * IndividualPage constructor.
87852ede8cSGreg Roach     *
88852ede8cSGreg Roach     * @param ClipboardService $clipboard_service
89852ede8cSGreg Roach     * @param ModuleService    $module_service
90852ede8cSGreg Roach     * @param UserService      $user_service
91852ede8cSGreg Roach     */
92852ede8cSGreg Roach    public function __construct(ClipboardService $clipboard_service, ModuleService $module_service, UserService $user_service)
93852ede8cSGreg Roach    {
94852ede8cSGreg Roach        $this->clipboard_service = $clipboard_service;
95852ede8cSGreg Roach        $this->module_service    = $module_service;
96852ede8cSGreg Roach        $this->user_service      = $user_service;
97852ede8cSGreg Roach    }
98852ede8cSGreg Roach
99852ede8cSGreg Roach    /**
100852ede8cSGreg Roach     * @param ServerRequestInterface $request
101852ede8cSGreg Roach     *
102852ede8cSGreg Roach     * @return ResponseInterface
103852ede8cSGreg Roach     */
104852ede8cSGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
105852ede8cSGreg Roach    {
106852ede8cSGreg Roach        $tree = $request->getAttribute('tree');
107852ede8cSGreg Roach        assert($tree instanceof Tree);
108852ede8cSGreg Roach
109852ede8cSGreg Roach        $xref = $request->getAttribute('xref');
110852ede8cSGreg Roach        assert(is_string($xref));
111852ede8cSGreg Roach
112*a091ac74SGreg Roach        $individual = Factory::individual()->make($xref, $tree);
113852ede8cSGreg Roach        $individual = Auth::checkIndividualAccess($individual);
114852ede8cSGreg Roach
115852ede8cSGreg Roach        // Redirect to correct xref/slug
116852ede8cSGreg Roach        if ($individual->xref() !== $xref || $request->getAttribute('slug') !== $individual->slug()) {
117e5d858f5SGreg Roach            return redirect($individual->url(), StatusCodeInterface::STATUS_MOVED_PERMANENTLY);
118852ede8cSGreg Roach        }
119852ede8cSGreg Roach
120852ede8cSGreg Roach        // What is (was) the age of the individual
121852ede8cSGreg Roach        $bdate = $individual->getBirthDate();
122852ede8cSGreg Roach        $ddate = $individual->getDeathDate();
123054771e9SGreg Roach
124054771e9SGreg Roach        if ($individual->isDead()) {
125852ede8cSGreg Roach            // If dead, show age at death
126054771e9SGreg Roach            $age = (new Age($bdate, $ddate))->ageAtEvent(false);
127852ede8cSGreg Roach        } else {
128054771e9SGreg Roach            // If living, show age today
12953432476SGreg Roach            $today = strtoupper(date('d M Y'));
13053432476SGreg Roach            $age   = (new Age($bdate, new Date($today)))->ageAtEvent(true);
131852ede8cSGreg Roach        }
132852ede8cSGreg Roach
133852ede8cSGreg Roach        // What images are linked to this individual
134852ede8cSGreg Roach        $individual_media = new Collection();
135852ede8cSGreg Roach        foreach ($individual->facts(['OBJE']) as $fact) {
136852ede8cSGreg Roach            $media_object = $fact->target();
137852ede8cSGreg Roach            if ($media_object instanceof Media) {
138852ede8cSGreg Roach                $media_file = $media_object->firstImageFile();
139852ede8cSGreg Roach                if ($media_file instanceof MediaFile) {
140852ede8cSGreg Roach                    $individual_media->add($media_file);
141852ede8cSGreg Roach                }
142852ede8cSGreg Roach            }
143852ede8cSGreg Roach        }
144852ede8cSGreg Roach
145852ede8cSGreg Roach        $name_records = new Collection();
146852ede8cSGreg Roach        foreach ($individual->facts(['NAME']) as $n => $name_fact) {
147852ede8cSGreg Roach            $name_records->add($this->formatNameRecord($tree, $n, $name_fact));
148852ede8cSGreg Roach        }
149852ede8cSGreg Roach
150852ede8cSGreg Roach        $sex_records = new Collection();
151852ede8cSGreg Roach        foreach ($individual->facts(['SEX']) as $n => $sex_fact) {
152852ede8cSGreg Roach            $sex_records->add($this->formatSexRecord($sex_fact));
153852ede8cSGreg Roach        }
154852ede8cSGreg Roach
155852ede8cSGreg Roach        // If this individual is linked to a user account, show the link
156852ede8cSGreg Roach        $user_link = '';
157852ede8cSGreg Roach        if (Auth::isAdmin()) {
158852ede8cSGreg Roach            $users = $this->user_service->findByIndividual($individual);
159852ede8cSGreg Roach            foreach ($users as $user) {
160852ede8cSGreg Roach                $user_link = ' —  <a href="' . e(route('admin-users', ['filter' => $user->email()])) . '">' . e($user->userName()) . '</a>';
161852ede8cSGreg Roach            }
162852ede8cSGreg Roach        }
163852ede8cSGreg Roach
164852ede8cSGreg Roach        return $this->viewResponse('individual-page', [
165852ede8cSGreg Roach            'age'              => $age,
166852ede8cSGreg Roach            'clipboard_facts'  => $this->clipboard_service->pastableFacts($individual, new Collection()),
167852ede8cSGreg Roach            'individual'       => $individual,
168852ede8cSGreg Roach            'individual_media' => $individual_media,
1692406e0e0SGreg Roach            'meta_description' => $this->metaDescription($individual),
170852ede8cSGreg Roach            'meta_robots'      => 'index,follow',
171852ede8cSGreg Roach            'name_records'     => $name_records,
172852ede8cSGreg Roach            'sex_records'      => $sex_records,
173852ede8cSGreg Roach            'sidebars'         => $this->getSidebars($individual),
174852ede8cSGreg Roach            'tabs'             => $this->getTabs($individual),
175852ede8cSGreg Roach            'significant'      => $this->significant($individual),
1765e6816beSGreg Roach            'title'            => $individual->fullName() . ' ' . $individual->lifespan(),
177852ede8cSGreg Roach            'tree'             => $tree,
178852ede8cSGreg Roach            'user_link'        => $user_link,
179852ede8cSGreg Roach        ]);
180852ede8cSGreg Roach    }
181852ede8cSGreg Roach
182852ede8cSGreg Roach    /**
1832406e0e0SGreg Roach     * @param Individual $individual
1842406e0e0SGreg Roach     *
1852406e0e0SGreg Roach     * @return string
1862406e0e0SGreg Roach     */
1872406e0e0SGreg Roach    private function metaDescription(Individual $individual): string
1882406e0e0SGreg Roach    {
1892406e0e0SGreg Roach        $meta_facts = [];
1902406e0e0SGreg Roach
1912406e0e0SGreg Roach        $birth_date  = $individual->getBirthDate();
1922406e0e0SGreg Roach        $birth_place = $individual->getBirthPlace();
1932406e0e0SGreg Roach
1942406e0e0SGreg Roach        if ($birth_date->isOK() || $birth_place->id() !== 0) {
1952406e0e0SGreg Roach            $meta_facts[] = I18N::translate('Birth') . ' ' .
1962406e0e0SGreg Roach                $birth_date->display(false, null, false) . ' ' .
1972406e0e0SGreg Roach                $birth_place->placeName();
1982406e0e0SGreg Roach        }
1992406e0e0SGreg Roach
2002406e0e0SGreg Roach        $death_date  = $individual->getDeathDate();
2012406e0e0SGreg Roach        $death_place = $individual->getDeathPlace();
2022406e0e0SGreg Roach
2032406e0e0SGreg Roach        if ($death_date->isOK() || $death_place->id() !== 0) {
2042406e0e0SGreg Roach            $meta_facts[] = I18N::translate('Death') . ' ' .
2052406e0e0SGreg Roach                $death_date->display(false, null, false) . ' ' .
2062406e0e0SGreg Roach                $death_place->placeName();
2072406e0e0SGreg Roach        }
2082406e0e0SGreg Roach
2092406e0e0SGreg Roach        foreach ($individual->childFamilies() as $family) {
2102406e0e0SGreg Roach            $meta_facts[] = I18N::translate('Parents') . ' ' . $family->fullName();
2112406e0e0SGreg Roach        }
2122406e0e0SGreg Roach
2132406e0e0SGreg Roach        foreach ($individual->spouseFamilies() as $family) {
2142406e0e0SGreg Roach            $spouse = $family->spouse($individual);
2152406e0e0SGreg Roach            if ($spouse instanceof Individual) {
2162406e0e0SGreg Roach                $meta_facts[] = I18N::translate('Spouse') . ' ' . $spouse->fullName();
2172406e0e0SGreg Roach            }
2182406e0e0SGreg Roach
2192406e0e0SGreg Roach            $child_names = $family->children()->map(static function (Individual $individual): string {
2202406e0e0SGreg Roach                return e($individual->getAllNames()[0]['givn']);
2212406e0e0SGreg Roach            })->implode(', ');
2222406e0e0SGreg Roach
2232406e0e0SGreg Roach
2242406e0e0SGreg Roach            if ($child_names !== '') {
2252406e0e0SGreg Roach                $meta_facts[] = I18N::translate('Children') . ' ' . $child_names;
2262406e0e0SGreg Roach            }
2272406e0e0SGreg Roach        }
2282406e0e0SGreg Roach
2292406e0e0SGreg Roach        $meta_facts = array_map('strip_tags', $meta_facts);
2302406e0e0SGreg Roach        $meta_facts = array_map('trim', $meta_facts);
2312406e0e0SGreg Roach
2322406e0e0SGreg Roach        return implode(', ', $meta_facts);
2332406e0e0SGreg Roach    }
2342406e0e0SGreg Roach
2352406e0e0SGreg Roach    /**
236852ede8cSGreg Roach     * Format a name record
237852ede8cSGreg Roach     *
238852ede8cSGreg Roach     * @param Tree $tree
239852ede8cSGreg Roach     * @param int  $n
240852ede8cSGreg Roach     * @param Fact $fact
241852ede8cSGreg Roach     *
242852ede8cSGreg Roach     * @return string
243852ede8cSGreg Roach     */
244852ede8cSGreg Roach    private function formatNameRecord(Tree $tree, $n, Fact $fact): string
245852ede8cSGreg Roach    {
246852ede8cSGreg Roach        $individual = $fact->record();
247852ede8cSGreg Roach
248852ede8cSGreg Roach        // Create a dummy record, so we can extract the formatted NAME value from it.
249*a091ac74SGreg Roach        $dummy = Factory::individual()->new(
250852ede8cSGreg Roach            'xref',
251852ede8cSGreg Roach            "0 @xref@ INDI\n1 DEAT Y\n" . $fact->gedcom(),
252852ede8cSGreg Roach            null,
253852ede8cSGreg Roach            $individual->tree()
254852ede8cSGreg Roach        );
255852ede8cSGreg Roach        $dummy->setPrimaryName(0); // Make sure we use the name from "1 NAME"
256852ede8cSGreg Roach
257852ede8cSGreg Roach        $container_class = 'card';
258852ede8cSGreg Roach        $content_class   = 'collapse';
259852ede8cSGreg Roach        $aria            = 'false';
260852ede8cSGreg Roach
261852ede8cSGreg Roach        if ($n === 0) {
262852ede8cSGreg Roach            $content_class = 'collapse show';
263852ede8cSGreg Roach            $aria          = 'true';
264852ede8cSGreg Roach        }
265852ede8cSGreg Roach        if ($fact->isPendingDeletion()) {
266852ede8cSGreg Roach            $container_class .= ' wt-old';
267852ede8cSGreg Roach        } elseif ($fact->isPendingAddition()) {
268852ede8cSGreg Roach            $container_class .= ' wt-new';
269852ede8cSGreg Roach        }
270852ede8cSGreg Roach
271852ede8cSGreg Roach        ob_start();
27260a3f275SGreg Roach        echo '<dl class="row mb-0"><dt class="col-md-4 col-lg-3">', I18N::translate('Name'), '</dt>';
27360a3f275SGreg Roach        echo '<dd class="col-md-8 col-lg-9">', $dummy->fullName(), '</dd>';
274852ede8cSGreg Roach        $ct = preg_match_all('/\n2 (\w+) (.*)/', $fact->gedcom(), $nmatch, PREG_SET_ORDER);
275852ede8cSGreg Roach        for ($i = 0; $i < $ct; $i++) {
276852ede8cSGreg Roach            $tag = $nmatch[$i][1];
277852ede8cSGreg Roach            if ($tag !== 'SOUR' && $tag !== 'NOTE' && $tag !== 'SPFX') {
27860a3f275SGreg Roach                echo '<dt class="col-md-4 col-lg-3">', GedcomTag::getLabel($tag, $individual), '</dt>';
27960a3f275SGreg Roach                echo '<dd class="col-md-8 col-lg-9">'; // Before using dir="auto" on this field, note that Gecko treats this as an inline element but WebKit treats it as a block element
280852ede8cSGreg Roach                if (isset($nmatch[$i][2])) {
281852ede8cSGreg Roach                    $name = e($nmatch[$i][2]);
282852ede8cSGreg Roach                    $name = str_replace('/', '', $name);
283852ede8cSGreg Roach                    $name = preg_replace('/(\S*)\*/', '<span class="starredname">\\1</span>', $name);
284852ede8cSGreg Roach                    switch ($tag) {
285852ede8cSGreg Roach                        case 'TYPE':
286852ede8cSGreg Roach                            echo GedcomCodeName::getValue($name, $individual);
287852ede8cSGreg Roach                            break;
288852ede8cSGreg Roach                        case 'SURN':
289852ede8cSGreg Roach                            // The SURN field is not necessarily the surname.
290852ede8cSGreg Roach                            // Where it is not a substring of the real surname, show it after the real surname.
291852ede8cSGreg Roach                            $surname = e($dummy->getAllNames()[0]['surname']);
292852ede8cSGreg Roach                            $surns   = preg_replace('/, */', ' ', $nmatch[$i][2]);
293852ede8cSGreg Roach                            if (strpos($dummy->getAllNames()[0]['surname'], $surns) !== false) {
294852ede8cSGreg Roach                                echo '<span dir="auto">' . $surname . '</span>';
295852ede8cSGreg Roach                            } else {
296852ede8cSGreg Roach                                echo I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $surname . '</span>', '<span dir="auto">' . $name . '</span>');
297852ede8cSGreg Roach                            }
298852ede8cSGreg Roach                            break;
299852ede8cSGreg Roach                        default:
300852ede8cSGreg Roach                            echo '<span dir="auto">' . $name . '</span>';
301852ede8cSGreg Roach                            break;
302852ede8cSGreg Roach                    }
303852ede8cSGreg Roach                }
304852ede8cSGreg Roach                echo '</dd>';
305852ede8cSGreg Roach            }
306852ede8cSGreg Roach        }
307852ede8cSGreg Roach        echo '</dl>';
308852ede8cSGreg Roach        if (strpos($fact->gedcom(), "\n2 SOUR") !== false) {
309852ede8cSGreg Roach            echo '<div id="indi_sour" class="clearfix">', FunctionsPrintFacts::printFactSources($tree, $fact->gedcom(), 2), '</div>';
310852ede8cSGreg Roach        }
311852ede8cSGreg Roach        if (strpos($fact->gedcom(), "\n2 NOTE") !== false) {
312852ede8cSGreg Roach            echo '<div id="indi_note" class="clearfix">', FunctionsPrint::printFactNotes($tree, $fact->gedcom(), 2), '</div>';
313852ede8cSGreg Roach        }
314852ede8cSGreg Roach        $content = ob_get_clean();
315852ede8cSGreg Roach
316852ede8cSGreg Roach        if ($fact->canEdit()) {
317852ede8cSGreg Roach            $edit_links =
318852ede8cSGreg Roach                '<a class="btn btn-link" href="#" data-confirm="' . I18N::translate('Are you sure you want to delete this fact?') . '" data-post-url="' . e(route(DeleteFact::class, ['tree' => $individual->tree()->name(), 'xref' => $individual->xref(), 'fact_id' => $fact->id()])) . '" title="' . I18N::translate('Delete this name') . '">' . view('icons/delete') . '<span class="sr-only">' . I18N::translate('Delete this name') . '</span></a>' .
319852ede8cSGreg Roach                '<a class="btn btn-link" href="' . e(route('edit-name', ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'tree' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the name') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the name') . '</span></a>';
320852ede8cSGreg Roach        } else {
321852ede8cSGreg Roach            $edit_links = '';
322852ede8cSGreg Roach        }
323852ede8cSGreg Roach
324852ede8cSGreg Roach        return '
325852ede8cSGreg Roach			<div class="' . $container_class . '">
326852ede8cSGreg Roach        <div class="card-header" role="tab" id="name-header-' . $n . '">
327852ede8cSGreg Roach		        <a data-toggle="collapse" data-parent="#individual-names" href="#name-content-' . $n . '" aria-expanded="' . $aria . '" aria-controls="name-content-' . $n . '">' . $dummy->fullName() . '</a>
328852ede8cSGreg Roach		      ' . $edit_links . '
329852ede8cSGreg Roach        </div>
330852ede8cSGreg Roach		    <div id="name-content-' . $n . '" class="' . $content_class . '" role="tabpanel" aria-labelledby="name-header-' . $n . '">
331852ede8cSGreg Roach		      <div class="card-body">' . $content . '</div>
332852ede8cSGreg Roach        </div>
333852ede8cSGreg Roach      </div>';
334852ede8cSGreg Roach    }
335852ede8cSGreg Roach
336852ede8cSGreg Roach    /**
337852ede8cSGreg Roach     * print information for a sex record
338852ede8cSGreg Roach     *
339852ede8cSGreg Roach     * @param Fact $fact
340852ede8cSGreg Roach     *
341852ede8cSGreg Roach     * @return string
342852ede8cSGreg Roach     */
343852ede8cSGreg Roach    private function formatSexRecord(Fact $fact): string
344852ede8cSGreg Roach    {
345852ede8cSGreg Roach        $individual = $fact->record();
346852ede8cSGreg Roach
347852ede8cSGreg Roach        switch ($fact->value()) {
348852ede8cSGreg Roach            case 'M':
349852ede8cSGreg Roach                $sex = I18N::translate('Male');
350852ede8cSGreg Roach                break;
351852ede8cSGreg Roach            case 'F':
352852ede8cSGreg Roach                $sex = I18N::translate('Female');
353852ede8cSGreg Roach                break;
354852ede8cSGreg Roach            default:
355852ede8cSGreg Roach                $sex = I18N::translateContext('unknown gender', 'Unknown');
356852ede8cSGreg Roach                break;
357852ede8cSGreg Roach        }
358852ede8cSGreg Roach
359852ede8cSGreg Roach        $container_class = 'card';
360852ede8cSGreg Roach        if ($fact->isPendingDeletion()) {
361852ede8cSGreg Roach            $container_class .= ' wt-old';
362852ede8cSGreg Roach        } elseif ($fact->isPendingAddition()) {
363852ede8cSGreg Roach            $container_class .= ' wt-new';
364852ede8cSGreg Roach        }
365852ede8cSGreg Roach
366852ede8cSGreg Roach        if ($individual->canEdit()) {
367852ede8cSGreg Roach            $edit_links = '<a class="btn btn-link" href="' . e(route(EditFact::class, ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'tree' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the gender') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the gender') . '</span></a>';
368852ede8cSGreg Roach        } else {
369852ede8cSGreg Roach            $edit_links = '';
370852ede8cSGreg Roach        }
371852ede8cSGreg Roach
372852ede8cSGreg Roach        return '
373852ede8cSGreg Roach		<div class="' . $container_class . '">
374852ede8cSGreg Roach			<div class="card-header" role="tab" id="name-header-add">
375852ede8cSGreg Roach				<div class="card-title mb-0">
376852ede8cSGreg Roach					<b>' . I18N::translate('Gender') . '</b> ' . $sex . $edit_links . '
377852ede8cSGreg Roach				</div>
378852ede8cSGreg Roach			</div>
379852ede8cSGreg Roach		</div>';
380852ede8cSGreg Roach    }
381852ede8cSGreg Roach
382852ede8cSGreg Roach    /**
383852ede8cSGreg Roach     * Which tabs should we show on this individual's page.
384852ede8cSGreg Roach     * We don't show empty tabs.
385852ede8cSGreg Roach     *
386852ede8cSGreg Roach     * @param Individual $individual
387852ede8cSGreg Roach     *
388b5c8fd7eSGreg Roach     * @return Collection<ModuleSidebarInterface>
389852ede8cSGreg Roach     */
390852ede8cSGreg Roach    public function getSidebars(Individual $individual): Collection
391852ede8cSGreg Roach    {
392852ede8cSGreg Roach        return $this->module_service->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
393852ede8cSGreg Roach            ->filter(static function (ModuleSidebarInterface $sidebar) use ($individual): bool {
394852ede8cSGreg Roach                return $sidebar->hasSidebarContent($individual);
395852ede8cSGreg Roach            });
396852ede8cSGreg Roach    }
397852ede8cSGreg Roach
398852ede8cSGreg Roach    /**
399852ede8cSGreg Roach     * Which tabs should we show on this individual's page.
400852ede8cSGreg Roach     * We don't show empty tabs.
401852ede8cSGreg Roach     *
402852ede8cSGreg Roach     * @param Individual $individual
403852ede8cSGreg Roach     *
404b5c8fd7eSGreg Roach     * @return Collection<ModuleTabInterface>
405852ede8cSGreg Roach     */
406852ede8cSGreg Roach    public function getTabs(Individual $individual): Collection
407852ede8cSGreg Roach    {
408852ede8cSGreg Roach        return $this->module_service->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
409852ede8cSGreg Roach            ->filter(static function (ModuleTabInterface $tab) use ($individual): bool {
410852ede8cSGreg Roach                return $tab->hasTabContent($individual);
411852ede8cSGreg Roach            });
412852ede8cSGreg Roach    }
413852ede8cSGreg Roach
414852ede8cSGreg Roach    /**
415852ede8cSGreg Roach     * What are the significant elements of this page?
416852ede8cSGreg Roach     * The layout will need them to generate URLs for charts and reports.
417852ede8cSGreg Roach     *
418852ede8cSGreg Roach     * @param Individual $individual
419852ede8cSGreg Roach     *
420852ede8cSGreg Roach     * @return stdClass
421852ede8cSGreg Roach     */
422852ede8cSGreg Roach    private function significant(Individual $individual): stdClass
423852ede8cSGreg Roach    {
424852ede8cSGreg Roach        [$surname] = explode(',', $individual->sortName());
425852ede8cSGreg Roach
426852ede8cSGreg Roach        $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first();
427852ede8cSGreg Roach
428852ede8cSGreg Roach        return (object) [
429852ede8cSGreg Roach            'family'     => $family,
430852ede8cSGreg Roach            'individual' => $individual,
431852ede8cSGreg Roach            'surname'    => $surname,
432852ede8cSGreg Roach        ];
433852ede8cSGreg Roach    }
434852ede8cSGreg Roach}
435