xref: /webtrees/app/Module/CensusAssistantModule.php (revision 15d603e7c7c15d20f055d3d9c38d6b133453c5be)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2017 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17namespace Fisharebest\Webtrees\Module;
18
19use Fisharebest\Webtrees\Census\Census;
20use Fisharebest\Webtrees\Census\CensusInterface;
21use Fisharebest\Webtrees\Family;
22use Fisharebest\Webtrees\Filter;
23use Fisharebest\Webtrees\FontAwesome;
24use Fisharebest\Webtrees\Functions\Functions;
25use Fisharebest\Webtrees\Functions\FunctionsDb;
26use Fisharebest\Webtrees\Functions\FunctionsEdit;
27use Fisharebest\Webtrees\GedcomRecord;
28use Fisharebest\Webtrees\I18N;
29use Fisharebest\Webtrees\Individual;
30use Fisharebest\Webtrees\Menu;
31use Fisharebest\Webtrees\Note;
32use Fisharebest\Webtrees\Soundex;
33
34/**
35 * Class CensusAssistantModule
36 */
37class CensusAssistantModule extends AbstractModule {
38    /** {@inheritdoc} */
39    public function getTitle() {
40        return /* I18N: Name of a module */
41            I18N::translate('Census assistant');
42    }
43
44    /** {@inheritdoc} */
45    public function getDescription() {
46        return /* I18N: Description of the “Census assistant” module */
47            I18N::translate('An alternative way to enter census transcripts and link them to individuals.');
48    }
49
50    /**
51     * This is a general purpose hook, allowing modules to respond to routes
52     * of the form module.php?mod=FOO&mod_action=BAR
53     *
54     * @param string $mod_action
55     */
56    public function modAction($mod_action) {
57        global $WT_TREE;
58
59        switch ($mod_action) {
60        case 'census-header':
61            header('Content-Type: text/html; charset=utf8');
62            $census = Filter::get('census');
63            echo $this->censusTableHeader(new $census);
64            break;
65
66        case 'census-individual':
67            header('Content-Type: text/html; charset=utf8');
68            $census     = Filter::get('census');
69            $individual = Individual::getInstance(Filter::get('xref'), $WT_TREE);
70            $head       = Individual::getInstance(Filter::get('head'), $WT_TREE);
71            echo $this->censusTableRow(new $census, $individual, $head);
72            break;
73
74        case 'media_find':
75            self::mediaFind();
76            break;
77        case 'media_query_3a':
78            self::mediaQuery();
79            break;
80        default:
81            http_response_code(404);
82        }
83    }
84
85    /**
86     * @param Individual      $individual
87     * @param CensusInterface $census
88     */
89    public function createCensusAssistant(Individual $individual) {
90        ?>
91
92        <div id="census-assistant-link" hidden>
93            <a href="#">
94                <?= I18N::translate('Create a shared note using the census assistant') ?>
95            </a>
96        </div>
97
98        <div id="census-assistant" hidden>
99            <input type="hidden" name="ca_census" id="ca-census">
100            <div class="form-group">
101                <div class="input-group">
102                    <label for="census-assistant-title" class="input-group-addon">
103                        <?= I18N::translate('Title') ?>
104                    </label>
105                    <input class="form-control" id="ca-title" name="ca_title" value="">
106                </div>
107            </div>
108
109            <div class="row">
110                <div class="form-group col-sm-6">
111                    <div class="input-group">
112                        <label for="census-assistant-citation" class="input-group-addon">
113                            <?= I18N::translate('Citation') ?>
114                        </label>
115                        <input class="form-control" id="census-assistant-citation" name="ca_citation">
116                    </div>
117                </div>
118
119                <div class="form-group col-sm-6">
120                    <div class="input-group">
121                        <label for="census-assistant-place" class="input-group-addon">
122                            <?= I18N::translate('Place') ?>
123                        </label>
124                        <input class="form-control" id="census-assistant-place" name="ca_place">
125                    </div>
126                </div>
127            </div>
128
129            <div class="form-group">
130                <div class="input-group">
131                    <span class="input-group-addon"><?= I18N::translate('Individuals') ?></span>
132                    <?= FunctionsEdit::formControlIndividual($individual, ['id' => 'census-assistant-individual', 'style' => 'width:100%']) ?>
133                    <span class="input-group-btn">
134						<button type="button" class="btn btn-primary" id="census-assistant-add">
135							<?= FontAwesome::semanticIcon('add', I18N::translate('Add')) ?>
136						</button>
137					</span>
138                    <span class="input-group-btn">
139						<button type="button" class="btn btn-primary" id="census-assistant-head"
140                                title="<?= I18N::translate('Head of household') ?>">
141							<?= FontAwesome::semanticIcon('individual', I18N::translate('Head of household')) ?>
142						</button>
143					</span>
144                </div>
145            </div>
146
147            <table class="table table-bordered table-small table-responsive wt-census-assistant-table"
148                   id="census-assistant-table">
149                <thead class="wt-census-assistant-header"></thead>
150                <tbody class="wt-census-assistant-body"></tbody>
151            </table>
152
153            <div class="form-group">
154                <div class="input-group">
155                    <label for="census-assistant-notes" class="input-group-addon">
156                        <?= I18N::translate('Notes') ?>
157                    </label>
158                    <input class="form-control" id="census-assistant-notes" name="ca_notes">
159                </div>
160            </div>
161        </div>
162
163        <script>
164            // When a census date/place is selected, activate the census-assistant
165            function censusAssistantSelect () {
166                var censusAssistantLink = document.querySelector('#census-assistant-link');
167                var censusAssistant     = document.querySelector('#census-assistant');
168                var censusOption        = this.options[this.selectedIndex];
169                var census              = censusOption.dataset.census;
170                var censusPlace         = censusOption.dataset.place;
171                var censusYear          = censusOption.value.substr(-4);
172
173                if (censusOption.value !== '') {
174                    censusAssistantLink.removeAttribute('hidden');
175                } else {
176                    censusAssistantLink.setAttribute('hidden', '');
177                }
178
179                censusAssistant.setAttribute('hidden', '');
180                document.querySelector('#ca-census').value = census;
181                document.querySelector('#ca-title').value  = censusYear + ' ' + censusPlace + ' - <?= I18N::translate('Census transcript') ?> - <?= strip_tags($individual->getFullName()) ?> - <?= I18N::translate('Household') ?>';
182
183                fetch('module.php?mod=GEDFact_assistant&mod_action=census-header&census=' + census)
184                    .then(function (response) {
185                        return response.text();
186                    })
187                    .then(function (text) {
188                        document.querySelector('#census-assistant-table thead').innerHTML = text;
189                        document.querySelector('#census-assistant-table tbody').innerHTML = '';
190                    });
191            }
192
193            // When the census assistant is activated, show the input fields
194            function censusAssistantLink () {
195                document.querySelector('#census-selector').setAttribute('hidden', '');
196                this.setAttribute('hidden', '');
197                document.getElementById('census-assistant').removeAttribute('hidden');
198                // Set the current individual as the head of household.
199                censusAssistantHead();
200
201                return false;
202            }
203
204            // Add the currently selected individual to the census
205            function censusAssistantAdd () {
206                var censusSelector = document.querySelector('#census-selector');
207                var census         = censusSelector.options[censusSelector.selectedIndex].dataset.census;
208                var indi_selector  = document.querySelector('#census-assistant-individual');
209                var xref           = indi_selector.options[indi_selector.selectedIndex].value;
210                var headTd         = document.querySelector('#census-assistant-table td');
211                var head           = headTd === null ? xref : headTd.innerHTML;
212
213                fetch('module.php?mod=GEDFact_assistant&mod_action=census-individual&census=' + census + '&xref=' + xref + '&head=' + head, {credentials: 'same-origin'})
214                    .then(function (response) {
215                        return response.text();
216                    })
217                    .then(function (text) {
218                        document.querySelector('#census-assistant-table tbody').innerHTML += text;
219                    });
220
221                return false;
222            }
223
224            // Set the currently selected individual as the head of household
225            function censusAssistantHead () {
226                var censusSelector = document.querySelector('#census-selector');
227                var census         = censusSelector.options[censusSelector.selectedIndex].dataset.census;
228                var indi_selector  = document.querySelector('#census-assistant-individual');
229                var xref           = indi_selector.options[indi_selector.selectedIndex].value;
230
231                fetch('module.php?mod=GEDFact_assistant&mod_action=census-individual&census=' + census + '&xref=' + xref + '&head=' + xref, {credentials: 'same-origin'})
232                    .then(function (response) {
233                        return response.text();
234                    })
235                    .then(function (text) {
236                        document.querySelector('#census-assistant-table tbody').innerHTML = text;
237                    });
238
239                return false;
240            }
241
242            document.querySelector('#census-selector').addEventListener('change', censusAssistantSelect);
243            document.querySelector('#census-assistant-link').addEventListener('click', censusAssistantLink);
244            document.querySelector('#census-assistant-add').addEventListener('click', censusAssistantAdd);
245            document.querySelector('#census-assistant-head').addEventListener('click', censusAssistantHead);
246        </script>
247        <?php
248    }
249
250    /**
251     * @param Individual $individual
252     * @param string     $newged
253     *
254     * @return string
255     */
256    public function updateCensusAssistant(Individual $individual, $fact_id, $newged, $keep_chan) {
257        $ca_title       = Filter::post('ca_title');
258        $ca_place       = Filter::post('ca_place');
259        $ca_citation    = Filter::post('ca_citation');
260        $ca_individuals = Filter::postArray('ca_individuals');
261        $ca_notes       = Filter::post('ca_notes');
262        $ca_census      = Filter::post('ca_census', 'Fisharebest\\\\Webtrees\\\\Census\\\\CensusOf[A-Za-z0-9]+');
263
264        if ($ca_census !== '' && !empty($ca_individuals)) {
265            $census = new $ca_census;
266
267            $note_text   = $this->createNoteText($census, $ca_title, $ca_place, $ca_citation, $ca_individuals, $ca_notes);
268            $note_gedcom = '0 @new@ NOTE ' . str_replace("\n", "\n1 CONT ", $note_text);
269            $note        = $individual->getTree()->createRecord($note_gedcom);
270
271            $newged .= "\n2 NOTE @" . $note->getXref() . '@';
272
273            // Add the census fact to the rest of the household
274            foreach (array_keys($ca_individuals) as $xref) {
275                if ($xref !== $individual->getXref()) {
276                    Individual::getInstance($xref, $individual->getTree())
277                        ->updateFact($fact_id, $newged, !$keep_chan);
278                }
279            }
280        }
281
282        return $newged;
283    }
284
285    /**
286     * @param CensusInterface $census
287     * @param string          $ca_title
288     * @param string          $ca_place
289     * @param string          $ca_citation
290     * @param string[][]      $ca_individuals
291     * @param string          $ca_notes
292     *
293     * @return string
294     */
295    private function createNoteText(CensusInterface $census, $ca_title, $ca_place, $ca_citation, $ca_individuals, $ca_notes) {
296        $text = $ca_title . "\n" . $ca_citation . "\n" . $ca_place . "\n\n.start_formatted_area.\n\n";
297
298        foreach ($census->columns() as $n => $column) {
299            if ($n > 0) {
300                $text .= '|';
301            }
302            $text .= '.b.' . $column->abbreviation();
303        }
304
305        foreach ($ca_individuals as $xref => $columns) {
306            $text .= "\n" . implode('|', $columns);
307        }
308
309        return $text . "\n.end_formatted_area.\n\n" . $ca_notes;
310    }
311
312    /**
313     * Find a media object.
314     */
315    private static function mediaFind() {
316        global $WT_TREE;
317
318        $controller = new SimpleController;
319        $filter     = Filter::get('filter');
320        $multiple   = Filter::getBool('multiple');
321
322        $controller
323            ->setPageTitle(I18N::translate('Find an individual'))
324            ->pageHeader();
325
326        ?>
327        <script>
328            function pasterow (id, name, gend, yob, age, bpl) {
329                window.opener.opener.insertRowToTable(id, name, '', gend, '', yob, age, 'Y', '', bpl);
330            }
331
332            function pasteid (id, name, thumb) {
333                if (thumb) {
334                    window.opener.paste_id(id, name, thumb);
335                    <?php if (!$multiple) {
336                    echo 'window.close();';
337                } ?>
338                } else {
339                    // GEDFact_assistant ========================
340                    if (window.opener.document.getElementById('addlinkQueue')) {
341                        window.opener.insertRowToTable(id, name);
342                    }
343                    window.opener.paste_id(id);
344                    if (window.opener.pastename) {
345                        window.opener.pastename(name);
346                    }
347                    <?php if (!$multiple) {
348                    echo 'window.close();';
349                } ?>
350                }
351            }
352
353            function checknames (frm) {
354                if (document.forms[0].subclick) {
355                    button = document.forms[0].subclick.value;
356                } else {
357                    button = '';
358                }
359                if (frm.filter.value.length < 2 && button !== 'all') {
360                    alert('<?= I18N::translate('Please enter more than one character.') ?>');
361                    frm.filter.focus();
362                    return false;
363                }
364                if (button == 'all') {
365                    frm.filter.value = '';
366                }
367                return true;
368            }
369        </script>
370
371        <?php
372        echo '<div>';
373        echo '<table class="list_table width90" border="0">';
374        echo '<tr><td style="padding: 10px;" class="facts_label03 width90">'; // start column for find text header
375        echo $controller->getPageTitle();
376        echo '</td>';
377        echo '</tr>';
378        echo '</table>';
379        echo '<br>';
380        echo '<button onclick="window.close();">', I18N::translate('close'), '</button>';
381        echo '<br>';
382
383        $filter       = trim($filter);
384        $filter_array = explode(' ', preg_replace('/ {2,}/', ' ', $filter));
385        echo '<table class="tabs_table width90"><tr>';
386        $myindilist = FunctionsDb::searchIndividualNames($filter_array, [$WT_TREE]);
387        if ($myindilist) {
388            echo '<td class="list_value_wrap"><ul>';
389            usort($myindilist, '\Fisharebest\Webtrees\GedcomRecord::compare');
390            foreach ($myindilist as $indi) {
391                $nam = Filter::escapeHtml($indi->getFullName());
392                echo "<li><a href=\"#\" onclick=\"pasterow(
393					'" . $indi->getXref() . "' ,
394					'" . $nam . "' ,
395					'" . $indi->getSex() . "' ,
396					'" . $indi->getBirthYear() . "' ,
397					'" . (1901 - $indi->getBirthYear()) . "' ,
398					'" . $indi->getBirthPlace() . "'); return false;\">
399					<b>" . $indi->getFullName() . '</b>&nbsp;&nbsp;&nbsp;';
400
401                $born = I18N::translate('Birth');
402                echo '</span><br><span class="list_item">', $born, ' ', $indi->getBirthYear(), '&nbsp;&nbsp;&nbsp;', $indi->getBirthPlace(), '</span></a></li>';
403                echo '<hr>';
404            }
405            echo '</ul></td></tr><tr><td class="list_label">', I18N::translate('Total individuals: %s', count($myindilist)), '</tr></td>';
406        } else {
407            echo '<td class="list_value_wrap">';
408            echo I18N::translate('No results found.');
409            echo '</td></tr>';
410        }
411        echo '</table>';
412        echo '</div>';
413    }
414
415    /**
416     * Search for a media object.
417     */
418    private static function mediaQuery() {
419        global $WT_TREE;
420
421        $iid2 = Filter::get('iid', WT_REGEX_XREF);
422
423        $controller = new SimpleController;
424        $controller
425            ->setPageTitle(I18N::translate('Link to an existing media object'))
426            ->pageHeader();
427
428        $record = GedcomRecord::getInstance($iid2, $WT_TREE);
429        if ($record) {
430            $headjs = '';
431            if ($record instanceof Family) {
432                if ($record->getHusband()) {
433                    $headjs = $record->getHusband()->getXref();
434                } elseif ($record->getWife()) {
435                    $headjs = $record->getWife()->getXref();
436                }
437            }
438            ?>
439            <script>
440                function insertId () {
441                    if (window.opener.document.getElementById('addlinkQueue')) {
442                        // alert('Please move this alert window and examine the contents of the pop-up window, then click OK')
443                        window.opener.insertRowToTable('<?= $record->getXref() ?>', '<?= htmlspecialchars($record->getFullName()) ?>', '<?= $headjs ?>');
444                        window.close();
445                    }
446                }
447            </script>
448            <?php
449        } else {
450            ?>
451            <script>
452                function insertId () {
453                    window.opener.alert('<?= $iid2 ?> - <?= I18N::translate('Not a valid individual, family, or source ID') ?>');
454                    window.close();
455                }
456            </script>
457            <?php
458        }
459        ?>
460        <script>window.onLoad = insertId();</script>
461        <?php
462    }
463
464    /**
465     * Convert custom markup into HTML
466     *
467     * @param Note $note
468     *
469     * @return string
470     */
471    public static function formatCensusNote(Note $note) {
472        if (preg_match('/(.*)((?:\n.*)*)\n\.start_formatted_area\.\n(.+)\n(.+(?:\n.+)*)\n.end_formatted_area\.((?:\n.*)*)/', $note->getNote(), $match)) {
473            // This looks like a census-assistant shared note
474            $title     = Filter::escapeHtml($match[1]);
475            $preamble  = Filter::escapeHtml($match[2]);
476            $header    = Filter::escapeHtml($match[3]);
477            $data      = Filter::escapeHtml($match[4]);
478            $postamble = Filter::escapeHtml($match[5]);
479
480            // Get the column headers for the census to which this note refers
481            // requires the fact place & date to match the specific census
482            // censusPlace() (Soundex match) and censusDate() functions
483            $fmt_headers = [];
484            /** @var GedcomRecord[] $linkedRecords */
485            $linkedRecords = array_merge($note->linkedIndividuals('NOTE'), $note->linkedFamilies('NOTE'));
486            $firstRecord   = array_shift($linkedRecords);
487            if ($firstRecord) {
488                $countryCode = '';
489                $date        = '';
490                foreach ($firstRecord->getFacts('CENS') as $fact) {
491                    if (trim($fact->getAttribute('NOTE'), '@') === $note->getXref()) {
492                        $date        = $fact->getAttribute('DATE');
493                        $place       = explode(',', strip_tags($fact->getPlace()->getFullName()));
494                        $countryCode = Soundex::daitchMokotoff(array_pop($place));
495                        break;
496                    }
497                }
498
499                foreach (Census::allCensusPlaces() as $censusPlace) {
500                    if (Soundex::compare($countryCode, Soundex::daitchMokotoff($censusPlace->censusPlace()))) {
501                        foreach ($censusPlace->allCensusDates() as $census) {
502                            if ($census->censusDate() == $date) {
503                                foreach ($census->columns() as $column) {
504                                    $abbrev = $column->abbreviation();
505                                    if ($abbrev) {
506                                        $description          = $column->title() ? $column->title() : I18N::translate('Description unavailable');
507                                        $fmt_headers[$abbrev] = '<span title="' . $description . '">' . $abbrev . '</span>';
508                                    }
509                                }
510                                break 2;
511                            }
512                        }
513                    }
514                }
515            }
516            // Substitute header labels and format as HTML
517            $thead = '<tr><th>' . strtr(str_replace('|', '</th><th>', $header), $fmt_headers) . '</th></tr>';
518            $thead = str_replace('.b.', '', $thead);
519
520            // Format data as HTML
521            $tbody = '';
522            foreach (explode("\n", $data) as $row) {
523                $tbody .= '<tr>';
524                foreach (explode('|', $row) as $column) {
525                    $tbody .= '<td>' . $column . '</td>';
526                }
527                $tbody .= '</tr>';
528            }
529
530            return
531                $title . "\n" . // The newline allows the framework to expand the details and turn the first line into a link
532                '<div class="markdown">' .
533                '<p>' . $preamble . '</p>' .
534                '<table>' .
535                '<thead>' . $thead . '</thead>' .
536                '<tbody>' . $tbody . '</tbody>' .
537                '</table>' .
538                '<p>' . $postamble . '</p>' .
539                '</div>';
540        } else {
541            // Not a census-assistant shared note - apply default formatting
542            return Filter::formatText($note->getNote(), $note->getTree());
543        }
544    }
545
546    /**
547     * Generate an HTML row of data for the census header
548     * Add prefix cell (store XREF and drag/drop)
549     * Add suffix cell (delete button)
550     *
551     * @param CensusInterface $census
552     *
553     * @return string
554     */
555    public static function censusTableHeader(CensusInterface $census) {
556        $html = '';
557        foreach ($census->columns() as $column) {
558            $html .= '<th class="wt-census-assistant-field" title="' . $column->title() . '">' . $column->abbreviation() . '</th>';
559        }
560
561        return '<tr class="wt-census-assistant-row"><th hidden></th>' . $html . '<th></th></tr>';
562    }
563
564    /**
565     * Generate an HTML row of data for the census
566     * Add prefix cell (store XREF and drag/drop)
567     * Add suffix cell (delete button)
568     *
569     * @param CensusInterface $census
570     *
571     * @return string
572     */
573    public static function censusTableEmptyRow(CensusInterface $census) {
574        return '<tr class="wt-census-assistant-row"><td hidden></td>' . str_repeat('<td class="wt-census-assistant-field"><input type="text" class="form-control wt-census-assistant-form-control"></td>', count($census->columns())) . '<td><a class="icon-remove" href="#" title="' . I18N::translate('Remove') . '"></a></td></tr>';
575    }
576
577    /**
578     * Generate an HTML row of data for the census
579     * Add prefix cell (store XREF and drag/drop)
580     * Add suffix cell (delete button)
581     *
582     * @param CensusInterface $census
583     * @param Individual      $individual
584     * @param Individual      $head
585     *
586     * @return string
587     */
588    public static function censusTableRow(CensusInterface $census, Individual $individual, Individual $head) {
589        $html = '';
590        foreach ($census->columns() as $column) {
591            $html .= '<td class="wt-census-assistant-field"><input class="form-control wt-census-assistant-form-control" type="text" value="' . $column->generate($individual, $head) . '" name="ca_individuals[' . $individual->getXref() . '][]"></td>';
592        }
593
594        return '<tr class="wt-census-assistant-row"><td class="wt-census-assistant-field" hidden>' . $individual->getXref() . '</td>' . $html . '<td class="wt-census-assistant-field"><a class="icon-remove" href="#" title="' . I18N::translate('Remove') . '"></a></td></tr>';
595    }
596}
597