people tags #387
This commit is contained in:
parent
5003f92f00
commit
073222ec6d
@ -124,6 +124,9 @@
|
||||
// full existing-location catalog ({id, name, latitude, longitude}[]), used to warn about a
|
||||
// new-location suggestion that's actually already catalogued - see _confirmNewLocationSuggestion
|
||||
var allGeoLocations = @Html.Raw(Model.AllLocationsJson);
|
||||
// full existing-people catalog ({id, name, birthYearInLHijri, deathYearInLHijri}[]), used both
|
||||
// to pick an existing person for a tag and as the "other side" of a new person's relatives
|
||||
var allGeoPeople = @Html.Raw(Model.AllPeopleJson);
|
||||
$(document).ready(function () {
|
||||
renderPendingGeoDateTags();
|
||||
setupLocationAutocomplete(
|
||||
@ -162,9 +165,162 @@
|
||||
}
|
||||
);
|
||||
}
|
||||
setupPersonAutocomplete(
|
||||
document.getElementById('geotag-person-search'),
|
||||
document.getElementById('geotag-person-select'),
|
||||
document.getElementById('geotag-person-results'),
|
||||
allGeoPeople,
|
||||
function (person) {
|
||||
// picking an existing person (or clearing the field) means whatever was
|
||||
// typed into the "new person" panel no longer applies to this suggestion
|
||||
var widget = document.getElementById('new-person-widget');
|
||||
if (person != null && widget.style.display !== 'none') {
|
||||
toggleNewPersonWidget();
|
||||
}
|
||||
},
|
||||
function (typedText) {
|
||||
var widget = document.getElementById('new-person-widget');
|
||||
if (widget.style.display === 'none' || widget.style.display === '') {
|
||||
toggleNewPersonWidget();
|
||||
}
|
||||
document.getElementById('geotag-new-person-name').value = typedText;
|
||||
}
|
||||
);
|
||||
// birth/death location pickers inside the "new person" panel - these only ever pick an
|
||||
// existing location (a brand-new-location-within-a-brand-new-person is one indirection too
|
||||
// many for this form), so an unmatched search has nothing to offer
|
||||
setupLocationAutocomplete(
|
||||
document.getElementById('geotag-new-person-birthlocation-search'),
|
||||
document.getElementById('geotag-new-person-birthlocation-id'),
|
||||
document.getElementById('geotag-new-person-birthlocation-results'),
|
||||
allGeoLocations,
|
||||
null,
|
||||
function (typedText) {
|
||||
upToast('این مکان در فهرست نیست. ابتدا آن را جدا (در بخش «مکان» بالا) بهعنوان مکان جدید پیشنهاد دهید.', 'error');
|
||||
}
|
||||
);
|
||||
setupLocationAutocomplete(
|
||||
document.getElementById('geotag-new-person-deathlocation-search'),
|
||||
document.getElementById('geotag-new-person-deathlocation-id'),
|
||||
document.getElementById('geotag-new-person-deathlocation-results'),
|
||||
allGeoLocations,
|
||||
null,
|
||||
function (typedText) {
|
||||
upToast('این مکان در فهرست نیست. ابتدا آن را جدا (در بخش «مکان» بالا) بهعنوان مکان جدید پیشنهاد دهید.', 'error');
|
||||
}
|
||||
);
|
||||
});
|
||||
var geoDateTagMap = null;
|
||||
var geoDateTagMarker = null;
|
||||
var personRelativeRowCounter = 0; // ever-increasing, so removed rows never clash with new ones
|
||||
|
||||
function toggleNewPersonWidget() {
|
||||
var widget = document.getElementById('new-person-widget');
|
||||
var isHidden = widget.style.display === 'none' || widget.style.display === '';
|
||||
widget.style.display = isHidden ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// adds one "relative or affiliation" row to the new-person panel. The other side of the
|
||||
// relation must already exist (see the hint text above the button) - nesting another
|
||||
// brand-new person inside this one's relatives is a rare-enough case that it's left for a
|
||||
// later pass; the backend already supports it via relatedPeople, just not from this form yet.
|
||||
function addPersonRelativeRow() {
|
||||
var idx = personRelativeRowCounter++;
|
||||
var row = document.createElement('div');
|
||||
row.id = 'person-relative-row-' + idx;
|
||||
row.className = 'up-card';
|
||||
row.style.marginTop = '8px';
|
||||
row.innerHTML =
|
||||
'<div class="up-field-row">' +
|
||||
'<div class="up-field">' +
|
||||
'<label>نوع رابطه</label>' +
|
||||
'<select id="person-relative-kind-' + idx + '" onchange="togglePersonRelativeKind(' + idx + ')">' +
|
||||
'<option value="family">خویشاوندی</option>' +
|
||||
'<option value="affiliation">غیرخویشاوندی (مثل وزارت، مشاورت)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="up-field" id="person-relative-familytype-wrap-' + idx + '">' +
|
||||
'<label>نوع خویشاوندی</label>' +
|
||||
'<select id="person-relative-familytype-' + idx + '" onchange="togglePersonRelativeDegreeHint(' + idx + ')">' +
|
||||
'<option value="Parent">پدر/مادر ↔ فرزند</option>' +
|
||||
'<option value="Sibling">خواهر/برادر</option>' +
|
||||
'<option value="Spouse">همسر</option>' +
|
||||
'<option value="Ancestor">نیا ↔ نواده (با فاصلهٔ نسل نامعلوم یا مشخص)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="up-field" id="person-relative-affiliationtype-wrap-' + idx + '" style="display:none;">' +
|
||||
'<label>نوع وابستگی</label>' +
|
||||
'<select id="person-relative-affiliationtype-' + idx + '">' +
|
||||
'<option value="Minister">وزیر</option>' +
|
||||
'<option value="Advisor">مشاور</option>' +
|
||||
'<option value="Courtier">درباری</option>' +
|
||||
'<option value="Patron">حامی</option>' +
|
||||
'<option value="Ally">متحد</option>' +
|
||||
'<option value="Rival">رقیب</option>' +
|
||||
'<option value="Other">سایر</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="up-field" id="person-relative-degreehint-wrap-' + idx + '" style="display:none;">' +
|
||||
'<label>تعداد نسل (اختیاری، فقط اگر دقیقاً معلوم است)</label>' +
|
||||
'<input type="text" id="person-relative-degreehint-' + idx + '" placeholder="مثلاً ۲ برای پدربزرگ/مادربزرگ" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="up-field-row" id="person-relative-direction-wrap-' + idx + '">' +
|
||||
'<div class="up-field">' +
|
||||
'<label>جهت رابطه</label>' +
|
||||
'<select id="person-relative-direction-' + idx + '">' +
|
||||
'<option value="newFirst">این فرد جدید، طرف اول رابطه است (مثلاً پدر/وزیر طرف دوم)</option>' +
|
||||
'<option value="otherFirst">فرد دیگر، طرف اول رابطه است (مثلاً پدر/وزیر این فرد جدید)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="up-field-row">' +
|
||||
'<div class="up-field">' +
|
||||
'<label>فرد دیگر (باید از قبل در فهرست موجود باشد)</label>' +
|
||||
'<div class="up-autocomplete-wrap">' +
|
||||
'<input type="text" id="person-relative-person-search-' + idx + '" autocomplete="off" placeholder="نام فرد را تایپ کنید..." />' +
|
||||
'<input type="hidden" id="person-relative-person-id-' + idx + '" />' +
|
||||
'<div id="person-relative-person-results-' + idx + '" class="up-autocomplete-results" style="display:none;"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="up-field">' +
|
||||
'<label>یادداشت (اختیاری)</label>' +
|
||||
'<input type="text" id="person-relative-note-' + idx + '" />' +
|
||||
'</div>' +
|
||||
'<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="removePersonRelativeRow(' + idx + ')">✕ حذف این رابطه</a>' +
|
||||
'</div>';
|
||||
document.getElementById('geotag-person-relatives-list').appendChild(row);
|
||||
|
||||
setupPersonAutocomplete(
|
||||
document.getElementById('person-relative-person-search-' + idx),
|
||||
document.getElementById('person-relative-person-id-' + idx),
|
||||
document.getElementById('person-relative-person-results-' + idx),
|
||||
allGeoPeople,
|
||||
null,
|
||||
function (typedText) {
|
||||
upToast('این فرد در فهرست نیست. طرف دیگر یک رابطه باید از قبل تعریفشده باشد؛ برای دو فرد تازه، ابتدا این فرم را برای یکی از آنها بهصورت جدا تکمیل و ثبت کنید.', 'error');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function removePersonRelativeRow(idx) {
|
||||
var row = document.getElementById('person-relative-row-' + idx);
|
||||
if (row) row.remove();
|
||||
}
|
||||
|
||||
function togglePersonRelativeKind(idx) {
|
||||
var kind = $('#person-relative-kind-' + idx).val();
|
||||
var isFamily = kind === 'family';
|
||||
$('#person-relative-familytype-wrap-' + idx).toggle(isFamily);
|
||||
$('#person-relative-affiliationtype-wrap-' + idx).toggle(!isFamily);
|
||||
togglePersonRelativeDegreeHint(idx);
|
||||
}
|
||||
|
||||
function togglePersonRelativeDegreeHint(idx) {
|
||||
var kind = $('#person-relative-kind-' + idx).val();
|
||||
var isAncestor = kind === 'family' && $('#person-relative-familytype-' + idx).val() === 'Ancestor';
|
||||
$('#person-relative-degreehint-wrap-' + idx).toggle(isAncestor);
|
||||
}
|
||||
|
||||
function toggleNewLocationWidget() {
|
||||
var widget = document.getElementById('new-location-widget');
|
||||
@ -293,19 +449,113 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// Builds the SuggestedPersonGraphJson payload (see GanjoorPoemGeoDateTagCorrection's doc
|
||||
// comment for the exact shape) from the "new person" panel and its relatives-list rows.
|
||||
// Returns null (with an error toast) if the panel is open but something required is missing;
|
||||
// the caller is expected to have already checked the panel is actually meant to be used.
|
||||
function buildPersonGraphJson() {
|
||||
var name = ($('#geotag-new-person-name').val() || '').trim();
|
||||
if (name == '') {
|
||||
upToast('لطفاً نام فرد جدید را وارد کنید.', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
var birthLocationId = $('#geotag-new-person-birthlocation-id').val();
|
||||
var deathLocationId = $('#geotag-new-person-deathlocation-id').val();
|
||||
|
||||
var person = {
|
||||
localKey: 'new',
|
||||
existingPersonId: null,
|
||||
name: name,
|
||||
description: $('#geotag-new-person-description').val() || null,
|
||||
wikiUrl: $('#geotag-new-person-wikiurl').val() || null,
|
||||
birthYearInLHijri: $('#geotag-new-person-birthyear').val() ? parseInt($('#geotag-new-person-birthyear').val()) : null,
|
||||
deathYearInLHijri: $('#geotag-new-person-deathyear').val() ? parseInt($('#geotag-new-person-deathyear').val()) : null,
|
||||
validBirthDate: $('#geotag-new-person-validbirth').is(':checked'),
|
||||
validDeathDate: $('#geotag-new-person-validdeath').is(':checked'),
|
||||
birthLocationId: birthLocationId ? parseInt(birthLocationId) : null,
|
||||
deathLocationId: deathLocationId ? parseInt(deathLocationId) : null,
|
||||
familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null
|
||||
};
|
||||
|
||||
var relatedPeople = [];
|
||||
var relatedPeopleByPersonId = {}; // existingPersonId -> localKey, so a person picked in two rows isn't duplicated
|
||||
var relations = [];
|
||||
|
||||
var rows = $('#geotag-person-relatives-list > div');
|
||||
for (var r = 0; r < rows.length; r++) {
|
||||
var idx = rows[r].id.replace('person-relative-row-', '');
|
||||
var otherPersonId = $('#person-relative-person-id-' + idx).val();
|
||||
if (!otherPersonId) {
|
||||
upToast('برای یکی از روابط افزودهشده، فرد دیگر انتخاب نشده است.', 'error');
|
||||
return null;
|
||||
}
|
||||
otherPersonId = parseInt(otherPersonId);
|
||||
|
||||
var otherLocalKey = relatedPeopleByPersonId[otherPersonId];
|
||||
if (!otherLocalKey) {
|
||||
otherLocalKey = 'existing_' + otherPersonId;
|
||||
relatedPeopleByPersonId[otherPersonId] = otherLocalKey;
|
||||
relatedPeople.push({ localKey: otherLocalKey, existingPersonId: otherPersonId });
|
||||
}
|
||||
|
||||
var kind = $('#person-relative-kind-' + idx).val();
|
||||
var direction = $('#person-relative-direction-' + idx).val();
|
||||
var person1 = direction === 'otherFirst' ? otherLocalKey : 'new';
|
||||
var person2 = direction === 'otherFirst' ? 'new' : otherLocalKey;
|
||||
var note = $('#person-relative-note-' + idx).val() || null;
|
||||
|
||||
if (kind === 'affiliation') {
|
||||
relations.push({
|
||||
kind: 'affiliation',
|
||||
person1: person1,
|
||||
person2: person2,
|
||||
affiliationType: $('#person-relative-affiliationtype-' + idx).val(),
|
||||
note: note
|
||||
});
|
||||
} else {
|
||||
var relationType = $('#person-relative-familytype-' + idx).val();
|
||||
var degreeHintStr = $('#person-relative-degreehint-' + idx).val();
|
||||
relations.push({
|
||||
kind: 'family',
|
||||
person1: person1,
|
||||
person2: person2,
|
||||
relationType: relationType,
|
||||
degreeHint: (relationType === 'Ancestor' && degreeHintStr) ? parseInt(degreeHintStr) : null,
|
||||
note: note
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({ person: person, relatedPeople: relatedPeople, relations: relations });
|
||||
}
|
||||
|
||||
function renderPendingGeoDateTags() {
|
||||
var container = document.getElementById('pending-geotag-suggestions');
|
||||
if (geoDateTagSuggestions.length == 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
var html = '<table><tr><th>وضعیت</th><th>بیت</th><th>مکان</th><th>سال</th><th>ماه</th><th>روز</th><th>#</th></tr>';
|
||||
var html = '<table><tr><th>وضعیت</th><th>بیت</th><th>مکان</th><th>فرد</th><th>سال</th><th>ماه</th><th>روز</th><th>#</th></tr>';
|
||||
for (var i = 0; i < geoDateTagSuggestions.length; i++) {
|
||||
var s = geoDateTagSuggestions[i];
|
||||
var status = s.markForDelete ? '🗑️ درخواست حذف' : '➕ پیشنهاد جدید';
|
||||
var coupletText = (s.coupletIndex != null && geoDateTagCoupletTexts[s.coupletIndex]) ? geoDateTagCoupletTexts[s.coupletIndex] : '— کل شعر —';
|
||||
var locationText = s.markForDelete ? '' : (s.locationId ? ((allGeoLocations.filter(function (l) { return l.id === s.locationId; })[0] || {}).name || ('مکان #' + s.locationId)) : (s.suggestedLocationName || ''));
|
||||
html += '<tr><td>' + status + '</td><td>' + coupletText + '</td><td>' + locationText + '</td><td>' + (s.lunarYear || '') + '</td><td>' + (s.lunarMonth || '') + '</td><td>' + (s.lunarDay || '') + '</td>' +
|
||||
var personText = '';
|
||||
if (!s.markForDelete) {
|
||||
if (s.personId) {
|
||||
personText = (allGeoPeople.filter(function (p) { return p.id === s.personId; })[0] || {}).name || ('فرد #' + s.personId);
|
||||
} else if (s.suggestedPersonGraphJson) {
|
||||
try {
|
||||
var parsedGraph = JSON.parse(s.suggestedPersonGraphJson);
|
||||
personText = 'فرد جدید: ' + (parsedGraph.person ? parsedGraph.person.name : '');
|
||||
} catch (e) {
|
||||
personText = 'فرد جدید';
|
||||
}
|
||||
}
|
||||
}
|
||||
html += '<tr><td>' + status + '</td><td>' + coupletText + '</td><td>' + locationText + '</td><td>' + personText + '</td><td>' + (s.lunarYear || '') + '</td><td>' + (s.lunarMonth || '') + '</td><td>' + (s.lunarDay || '') + '</td>' +
|
||||
'<td><a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="removeGeoDateTagSuggestion(' + i + ')">✕ حذف از فهرست</a></td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
@ -350,13 +600,16 @@
|
||||
var year = $('#geotag-year').val();
|
||||
var month = $('#geotag-month').val();
|
||||
var day = $('#geotag-day').val();
|
||||
var personId = $('#geotag-person-select').val();
|
||||
var newPersonName = $('#geotag-new-person-name').val();
|
||||
|
||||
var hasLocation = (locationId && locationId != '') || (newLocationName && newLocationLat && newLocationLng);
|
||||
var hasDate = year && year != '';
|
||||
var hasPerson = (personId && personId != '') || (newPersonName && newPersonName.trim() != '');
|
||||
|
||||
if (!hasLocation && !hasDate) {
|
||||
if (!hasLocation && !hasDate && !hasPerson) {
|
||||
if (showErrorIfEmpty) {
|
||||
upToast('لطفاً حداقل یک مکان یا سال وارد کنید.', 'error');
|
||||
upToast('لطفاً حداقل یک مکان، تاریخ یا فرد وارد کنید.', 'error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -372,6 +625,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
var suggestedPersonGraphJson = null;
|
||||
if ((!personId || personId == '') && newPersonName && newPersonName.trim() != '') {
|
||||
suggestedPersonGraphJson = buildPersonGraphJson();
|
||||
if (suggestedPersonGraphJson == null) {
|
||||
return null; // buildPersonGraphJson already showed the relevant error toast
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
markForDelete: false,
|
||||
coupletIndex: (coupletIndex && coupletIndex != '') ? parseInt(coupletIndex) : null,
|
||||
@ -382,6 +643,8 @@
|
||||
lunarYear: year ? parseInt(year) : null,
|
||||
lunarMonth: month ? parseInt(month) : null,
|
||||
lunarDay: day ? parseInt(day) : null,
|
||||
personId: (personId && personId != '') ? parseInt(personId) : null,
|
||||
suggestedPersonGraphJson: suggestedPersonGraphJson,
|
||||
ignoreInCategory: $('#geotag-ignore-in-category').is(':checked'),
|
||||
suggestionNote: $('#geotag-note').val() || null
|
||||
};
|
||||
@ -398,6 +661,27 @@
|
||||
$('#geotag-year').val('');
|
||||
$('#geotag-month').val('');
|
||||
$('#geotag-day').val('');
|
||||
$('#geotag-person-select').val('');
|
||||
$('#geotag-person-search').val('');
|
||||
$('#geotag-person-results').hide().empty();
|
||||
$('#geotag-new-person-name').val('');
|
||||
$('#geotag-new-person-description').val('');
|
||||
$('#geotag-new-person-wikiurl').val('');
|
||||
$('#geotag-new-person-birthyear').val('');
|
||||
$('#geotag-new-person-deathyear').val('');
|
||||
$('#geotag-new-person-validbirth').prop('checked', false);
|
||||
$('#geotag-new-person-validdeath').prop('checked', false);
|
||||
$('#geotag-new-person-birthlocation-id').val('');
|
||||
$('#geotag-new-person-birthlocation-search').val('');
|
||||
$('#geotag-new-person-birthlocation-results').hide().empty();
|
||||
$('#geotag-new-person-deathlocation-id').val('');
|
||||
$('#geotag-new-person-deathlocation-search').val('');
|
||||
$('#geotag-new-person-deathlocation-results').hide().empty();
|
||||
$('#geotag-new-person-familytreecaption').val('');
|
||||
$('#geotag-person-relatives-list').empty();
|
||||
if (document.getElementById('new-person-widget').style.display !== 'none') {
|
||||
toggleNewPersonWidget();
|
||||
}
|
||||
$('#geotag-ignore-in-category').prop('checked', false);
|
||||
$('#geotag-note').val('');
|
||||
if (geoDateTagMarker != null && geoDateTagMap != null) {
|
||||
@ -2106,6 +2390,73 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="up-field">
|
||||
<label for="geotag-person-search">فرد</label>
|
||||
<div class="up-autocomplete-wrap">
|
||||
<input type="text" id="geotag-person-search" autocomplete="off" placeholder="نام فرد را تایپ کنید..." />
|
||||
<input type="hidden" id="geotag-person-select" />
|
||||
<div id="geotag-person-results" class="up-autocomplete-results" style="display:none;"></div>
|
||||
</div>
|
||||
<p class="up-hint">شروع به تایپ کنید تا فهرست افراد بر اساس نام فیلتر شود؛ اگر فرد مورد نظر پیدا نشد، گزینهٔ تعریف او بهعنوان فرد جدید نمایش داده میشود.</p>
|
||||
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="toggleNewPersonWidget()">👤 فرد جدید پیشنهاد بده</a>
|
||||
</div>
|
||||
|
||||
<div id="new-person-widget" style="display:none;">
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-name">نام</label>
|
||||
<input type="text" id="geotag-new-person-name" />
|
||||
</div>
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-description">توضیحات (اختیاری)</label>
|
||||
<textarea id="geotag-new-person-description" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-wikiurl">لینک ویکیپدیا (اختیاری)</label>
|
||||
<input type="text" id="geotag-new-person-wikiurl" />
|
||||
</div>
|
||||
<div class="up-field-row">
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-birthyear">سال تولد (قمری)</label>
|
||||
<input type="text" id="geotag-new-person-birthyear" />
|
||||
<span class="up-check-row"><input type="checkbox" id="geotag-new-person-validbirth" /><label for="geotag-new-person-validbirth">تاریخ تولد قطعی است</label></span>
|
||||
</div>
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-deathyear">سال وفات (قمری)</label>
|
||||
<input type="text" id="geotag-new-person-deathyear" />
|
||||
<span class="up-check-row"><input type="checkbox" id="geotag-new-person-validdeath" /><label for="geotag-new-person-validdeath">تاریخ وفات قطعی است</label></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="up-field-row">
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-birthlocation-search">محل تولد (اختیاری، فقط از مکانهای موجود در فهرست)</label>
|
||||
<div class="up-autocomplete-wrap">
|
||||
<input type="text" id="geotag-new-person-birthlocation-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
|
||||
<input type="hidden" id="geotag-new-person-birthlocation-id" />
|
||||
<div id="geotag-new-person-birthlocation-results" class="up-autocomplete-results" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-deathlocation-search">محل وفات (اختیاری، فقط از مکانهای موجود در فهرست)</label>
|
||||
<div class="up-autocomplete-wrap">
|
||||
<input type="text" id="geotag-new-person-deathlocation-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
|
||||
<input type="hidden" id="geotag-new-person-deathlocation-id" />
|
||||
<div id="geotag-new-person-deathlocation-results" class="up-autocomplete-results" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="up-field">
|
||||
<label for="geotag-new-person-familytreecaption">عنوان شجرهنامه (اختیاری، فقط اگر این فرد ریشهٔ یک خانواده/سلسله شناختهشده است - مثلاً «ساسانیان»)</label>
|
||||
<input type="text" id="geotag-new-person-familytreecaption" />
|
||||
</div>
|
||||
|
||||
<p class="up-hint">
|
||||
اگر خویشاوندی یا وابستگی این فرد با فردی دیگر را میدانید، میتوانید همینجا اضافه کنید (فرد دیگر باید از قبل در فهرست موجود باشد؛
|
||||
برای فردی که او هم تازه است، ابتدا این فرم را برای او هم بهصورت جدا تکمیل و ثبت کنید، سپس این رابطه را اضافه کنید).
|
||||
</p>
|
||||
<div id="geotag-person-relatives-list"></div>
|
||||
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="addPersonRelativeRow()">➕ افزودن خویشاوند یا وابستگی</a>
|
||||
</div>
|
||||
|
||||
<div class="up-check-row">
|
||||
<input type="checkbox" id="geotag-ignore-in-category" />
|
||||
<label for="geotag-ignore-in-category">در نقشهٔ کلی مجموعه نادیده گرفته شود (مثلاً مکانی که فقط برای مقایسه ذکر شده)</label>
|
||||
|
||||
@ -131,6 +131,22 @@ namespace GanjooRazor.Areas.User.Pages
|
||||
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// people (for the geo/date/person tag's person picker)
|
||||
/// </summary>
|
||||
public List<GanjoorRelatedPerson> People { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// camelCase JSON of People (id/name/birthYearInLHijri/deathYearInLHijri only - enough to
|
||||
/// tell two same-named people apart in the picker), same pre-serialization rationale as
|
||||
/// AllLocationsJson
|
||||
/// </summary>
|
||||
public string AllPeopleJson =>
|
||||
JsonConvert.SerializeObject(
|
||||
(People ?? new List<GanjoorRelatedPerson>()).Select(p => new { p.Id, p.Name, p.BirthYearInLHijri, p.DeathYearInLHijri }),
|
||||
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
|
||||
);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// poem geo date tags
|
||||
@ -352,6 +368,15 @@ namespace GanjooRazor.Areas.User.Pages
|
||||
|
||||
Locations.AddRange(JsonConvert.DeserializeObject<GanjoorGeoLocation[]>(await responseLocations.Content.ReadAsStringAsync()));
|
||||
|
||||
var responsePeople = await secureClient.GetAsync($"{APIRoot.Url}/api/people");
|
||||
if (!responsePeople.IsSuccessStatusCode)
|
||||
{
|
||||
FatalError = JsonConvert.DeserializeObject<string>(await responsePeople.Content.ReadAsStringAsync());
|
||||
return Page();
|
||||
}
|
||||
|
||||
People = JsonConvert.DeserializeObject<List<GanjoorRelatedPerson>>(await responsePeople.Content.ReadAsStringAsync());
|
||||
|
||||
|
||||
var tagsResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poem/{PageInformation.Id}/geotag");
|
||||
if (!tagsResponse.IsSuccessStatusCode)
|
||||
|
||||
@ -2599,3 +2599,100 @@ function setupLocationAutocomplete(searchInput, hiddenInput, resultsBox, locatio
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// same search-as-you-type / "not found -> define new" pattern as setupLocationAutocomplete above,
|
||||
// but for picking a GanjoorRelatedPerson (people don't have coordinates, so the result rows and the
|
||||
// "not found" wording differ enough that sharing one function would need special-casing throughout)
|
||||
function setupPersonAutocomplete(searchInput, hiddenInput, resultsBox, people, onSelect, onNotFound) {
|
||||
var MAX_RESULTS = 12;
|
||||
|
||||
function hide() {
|
||||
resultsBox.style.display = 'none';
|
||||
resultsBox.innerHTML = '';
|
||||
}
|
||||
|
||||
function personSubtitle(p) {
|
||||
if (p.birthYearInLHijri && p.deathYearInLHijri) {
|
||||
return p.birthYearInLHijri + ' - ' + p.deathYearInLHijri;
|
||||
}
|
||||
if (p.birthYearInLHijri) {
|
||||
return 'تولد ' + p.birthYearInLHijri;
|
||||
}
|
||||
if (p.deathYearInLHijri) {
|
||||
return 'وفات ' + p.deathYearInLHijri;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function selectPerson(p) {
|
||||
searchInput.value = p.name;
|
||||
hiddenInput.value = String(p.id);
|
||||
hide();
|
||||
if (onSelect) onSelect(p);
|
||||
}
|
||||
|
||||
function render(typedText) {
|
||||
var typedLower = typedText.toLowerCase();
|
||||
var matches = (people || []).filter(function (p) {
|
||||
return p.name && p.name.toLowerCase().indexOf(typedLower) !== -1;
|
||||
});
|
||||
|
||||
if (matches.length == 0) {
|
||||
var emptyRow = document.createElement('div');
|
||||
emptyRow.className = 'up-autocomplete-item up-autocomplete-item--empty';
|
||||
emptyRow.textContent = '➕ «' + typedText + '» در فهرست پیدا نشد؛ بهعنوان فرد جدید تعریف شود';
|
||||
emptyRow.addEventListener('click', function () {
|
||||
hide();
|
||||
if (onNotFound) onNotFound(typedText);
|
||||
});
|
||||
resultsBox.innerHTML = '';
|
||||
resultsBox.appendChild(emptyRow);
|
||||
resultsBox.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsBox.innerHTML = '';
|
||||
for (var i = 0; i < Math.min(matches.length, MAX_RESULTS); i++) {
|
||||
var p = matches[i];
|
||||
var subtitle = personSubtitle(p);
|
||||
var row = document.createElement('div');
|
||||
row.className = 'up-autocomplete-item';
|
||||
row.textContent = subtitle ? (p.name + ' (' + subtitle + ')') : p.name;
|
||||
row.addEventListener('click', function (person) {
|
||||
return function () { selectPerson(person); };
|
||||
}(p));
|
||||
resultsBox.appendChild(row);
|
||||
}
|
||||
if (matches.length > MAX_RESULTS) {
|
||||
var moreRow = document.createElement('div');
|
||||
moreRow.className = 'up-autocomplete-item up-autocomplete-item--more';
|
||||
moreRow.textContent = '… و ' + (matches.length - MAX_RESULTS) + ' مورد دیگر (برای محدودکردن نتایج بیشتر تایپ کنید)';
|
||||
resultsBox.appendChild(moreRow);
|
||||
}
|
||||
resultsBox.style.display = 'block';
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', function () {
|
||||
hiddenInput.value = ''; // typing invalidates whatever was previously selected
|
||||
var typed = searchInput.value.trim();
|
||||
if (typed == '') {
|
||||
hide();
|
||||
if (onSelect) onSelect(null);
|
||||
return;
|
||||
}
|
||||
render(typed);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('focus', function () {
|
||||
var typed = searchInput.value.trim();
|
||||
if (typed != '' && hiddenInput.value == '') {
|
||||
render(typed); // re-show suggestions for text that hasn't resolved to a pick yet
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target !== searchInput && !resultsBox.contains(e.target)) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
64
RMuseum/Controllers/GanjoorRelatedPersonController.cs
Normal file
64
RMuseum/Controllers/GanjoorRelatedPersonController.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RMuseum.Models.Ganjoor;
|
||||
using RMuseum.Services;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RMuseum.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// related people (family tree / person tagging) - read-only for now, see IGanjoorRelatedPersonService
|
||||
/// </summary>
|
||||
[Produces("application/json")]
|
||||
[Route("api/people")]
|
||||
public class GanjoorRelatedPersonController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// get all people
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson[]))]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
||||
public async Task<IActionResult> GetPeopleAsync()
|
||||
{
|
||||
var res = await _personService.GetPeopleAsync();
|
||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
||||
return BadRequest(res.ExceptionString);
|
||||
return Ok(res.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get person by id
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{id}")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson))]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
||||
public async Task<IActionResult> GetPersonAsync(int id)
|
||||
{
|
||||
var res = await _personService.GetPersonAsync(id);
|
||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
||||
return BadRequest(res.ExceptionString);
|
||||
return Ok(res.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// person service
|
||||
/// </summary>
|
||||
private readonly IGanjoorRelatedPersonService _personService;
|
||||
|
||||
/// <summary>
|
||||
/// constructor
|
||||
/// </summary>
|
||||
/// <param name="personService"></param>
|
||||
public GanjoorRelatedPersonController(IGanjoorRelatedPersonService personService)
|
||||
{
|
||||
_personService = personService;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
RMuseum/Services/IGanjoorRelatedPersonService.cs
Normal file
27
RMuseum/Services/IGanjoorRelatedPersonService.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using RMuseum.Models.Ganjoor;
|
||||
using RSecurityBackend.Models.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RMuseum.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// related people (family tree / person tagging) service - read-only for now: the only way a
|
||||
/// new person is created is via the geo/date/person tag correction's SuggestedPersonGraphJson,
|
||||
/// materialized on moderator approval (see GanjoorService-ModeratePoemCorrection.cs)
|
||||
/// </summary>
|
||||
public interface IGanjoorRelatedPersonService
|
||||
{
|
||||
/// <summary>
|
||||
/// get all people (for the search-as-you-type person picker)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<RServiceResult<GanjoorRelatedPerson[]>> GetPeopleAsync();
|
||||
|
||||
/// <summary>
|
||||
/// get person by id
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
Task<RServiceResult<GanjoorRelatedPerson>> GetPersonAsync(int id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RMuseum.DbContext;
|
||||
using RMuseum.Models.Ganjoor;
|
||||
using RSecurityBackend.Models.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RMuseum.Services.Implementation
|
||||
{
|
||||
/// <summary>
|
||||
/// related people service implementation
|
||||
/// </summary>
|
||||
public class GanjoorRelatedPersonService : IGanjoorRelatedPersonService
|
||||
{
|
||||
/// <summary>
|
||||
/// get all people
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<RServiceResult<GanjoorRelatedPerson[]>> GetPeopleAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new RServiceResult<GanjoorRelatedPerson[]>
|
||||
(
|
||||
await _context.GanjoorRelatedPersons
|
||||
.OrderBy(p => p.Name).ToArrayAsync()
|
||||
);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return new RServiceResult<GanjoorRelatedPerson[]>(null, exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get person by id
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<RServiceResult<GanjoorRelatedPerson>> GetPersonAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new RServiceResult<GanjoorRelatedPerson>
|
||||
(
|
||||
await _context.GanjoorRelatedPersons
|
||||
.Where(p => p.Id == id)
|
||||
.SingleOrDefaultAsync()
|
||||
);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return new RServiceResult<GanjoorRelatedPerson>(null, exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database Context
|
||||
/// </summary>
|
||||
protected readonly RMuseumDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// constructor
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public GanjoorRelatedPersonService(RMuseumDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -300,6 +300,9 @@ namespace RMuseum
|
||||
//geo location service
|
||||
services.AddTransient<IGeoLocationService, GeoLocationService>();
|
||||
|
||||
//related people (family tree / person tagging) service
|
||||
services.AddTransient<IGanjoorRelatedPersonService, GanjoorRelatedPersonService>();
|
||||
|
||||
//tracking service
|
||||
services.AddTransient<IUserVisitsTrackingService, UserVisitsTrackingService>();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user