diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml b/GanjooRazor/Areas/User/Pages/Editor.cshtml
index db506606..5356d1dd 100644
--- a/GanjooRazor/Areas/User/Pages/Editor.cshtml
+++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml
@@ -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 =
+ '
' +
+ '
' +
+ 'نوع رابطه ' +
+ '' +
+ 'خویشاوندی ' +
+ 'غیرخویشاوندی (مثل وزارت، مشاورت) ' +
+ ' ' +
+ '
' +
+ '
' +
+ 'نوع خویشاوندی ' +
+ '' +
+ 'پدر/مادر ↔ فرزند ' +
+ 'خواهر/برادر ' +
+ 'همسر ' +
+ 'نیا ↔ نواده (با فاصلهٔ نسل نامعلوم یا مشخص) ' +
+ ' ' +
+ '
' +
+ '
' +
+ 'نوع وابستگی ' +
+ '' +
+ 'وزیر ' +
+ 'مشاور ' +
+ 'درباری ' +
+ 'حامی ' +
+ 'متحد ' +
+ 'رقیب ' +
+ 'سایر ' +
+ ' ' +
+ '
' +
+ '
' +
+ 'تعداد نسل (اختیاری، فقط اگر دقیقاً معلوم است) ' +
+ ' ' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ 'جهت رابطه ' +
+ '' +
+ 'این فرد جدید، طرف اول رابطه است (مثلاً پدر/وزیر طرف دوم) ' +
+ 'فرد دیگر، طرف اول رابطه است (مثلاً پدر/وزیر این فرد جدید) ' +
+ ' ' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
فرد دیگر (باید از قبل در فهرست موجود باشد) ' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ 'یادداشت (اختیاری) ' +
+ ' ' +
+ '
' +
+ '
✕ حذف این رابطه ' +
+ '
';
+ 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 = 'وضعیت بیت مکان سال ماه روز # ';
+ var html = 'وضعیت بیت مکان فرد سال ماه روز # ';
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 += '' + status + ' ' + coupletText + ' ' + locationText + ' ' + (s.lunarYear || '') + ' ' + (s.lunarMonth || '') + ' ' + (s.lunarDay || '') + ' ' +
+ 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 += '' + status + ' ' + coupletText + ' ' + locationText + ' ' + personText + ' ' + (s.lunarYear || '') + ' ' + (s.lunarMonth || '') + ' ' + (s.lunarDay || '') + ' ' +
'✕ حذف از فهرست ';
}
html += '
';
@@ -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
+
+
فرد
+
+
شروع به تایپ کنید تا فهرست افراد بر اساس نام فیلتر شود؛ اگر فرد مورد نظر پیدا نشد، گزینهٔ تعریف او بهعنوان فرد جدید نمایش داده میشود.
+
👤 فرد جدید پیشنهاد بده
+
+
+
+
در نقشهٔ کلی مجموعه نادیده گرفته شود (مثلاً مکانی که فقط برای مقایسه ذکر شده)
diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs b/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs
index bf71031c..3b73cbf5 100644
--- a/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs
+++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs
@@ -131,6 +131,22 @@ namespace GanjooRazor.Areas.User.Pages
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
);
+ ///
+ /// people (for the geo/date/person tag's person picker)
+ ///
+ public List
People { get; set; }
+
+ ///
+ /// 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
+ ///
+ public string AllPeopleJson =>
+ JsonConvert.SerializeObject(
+ (People ?? new List()).Select(p => new { p.Id, p.Name, p.BirthYearInLHijri, p.DeathYearInLHijri }),
+ new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
+ );
+
///
/// poem geo date tags
@@ -352,6 +368,15 @@ namespace GanjooRazor.Areas.User.Pages
Locations.AddRange(JsonConvert.DeserializeObject(await responseLocations.Content.ReadAsStringAsync()));
+ var responsePeople = await secureClient.GetAsync($"{APIRoot.Url}/api/people");
+ if (!responsePeople.IsSuccessStatusCode)
+ {
+ FatalError = JsonConvert.DeserializeObject(await responsePeople.Content.ReadAsStringAsync());
+ return Page();
+ }
+
+ People = JsonConvert.DeserializeObject>(await responsePeople.Content.ReadAsStringAsync());
+
var tagsResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poem/{PageInformation.Id}/geotag");
if (!tagsResponse.IsSuccessStatusCode)
diff --git a/GanjooRazor/wwwroot/js/bk.js b/GanjooRazor/wwwroot/js/bk.js
index c254aa48..39b0b84f 100644
--- a/GanjooRazor/wwwroot/js/bk.js
+++ b/GanjooRazor/wwwroot/js/bk.js
@@ -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();
+ }
+ });
+}
diff --git a/RMuseum/Controllers/GanjoorRelatedPersonController.cs b/RMuseum/Controllers/GanjoorRelatedPersonController.cs
new file mode 100644
index 00000000..2e909cd5
--- /dev/null
+++ b/RMuseum/Controllers/GanjoorRelatedPersonController.cs
@@ -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
+{
+ ///
+ /// related people (family tree / person tagging) - read-only for now, see IGanjoorRelatedPersonService
+ ///
+ [Produces("application/json")]
+ [Route("api/people")]
+ public class GanjoorRelatedPersonController : Controller
+ {
+ ///
+ /// get all people
+ ///
+ ///
+ [HttpGet]
+ [AllowAnonymous]
+ [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson[]))]
+ [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
+ public async Task GetPeopleAsync()
+ {
+ var res = await _personService.GetPeopleAsync();
+ if (!string.IsNullOrEmpty(res.ExceptionString))
+ return BadRequest(res.ExceptionString);
+ return Ok(res.Result);
+ }
+
+ ///
+ /// get person by id
+ ///
+ ///
+ ///
+ [HttpGet("{id}")]
+ [AllowAnonymous]
+ [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson))]
+ [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
+ public async Task GetPersonAsync(int id)
+ {
+ var res = await _personService.GetPersonAsync(id);
+ if (!string.IsNullOrEmpty(res.ExceptionString))
+ return BadRequest(res.ExceptionString);
+ return Ok(res.Result);
+ }
+
+ ///
+ /// person service
+ ///
+ private readonly IGanjoorRelatedPersonService _personService;
+
+ ///
+ /// constructor
+ ///
+ ///
+ public GanjoorRelatedPersonController(IGanjoorRelatedPersonService personService)
+ {
+ _personService = personService;
+ }
+ }
+}
diff --git a/RMuseum/Services/IGanjoorRelatedPersonService.cs b/RMuseum/Services/IGanjoorRelatedPersonService.cs
new file mode 100644
index 00000000..7537526e
--- /dev/null
+++ b/RMuseum/Services/IGanjoorRelatedPersonService.cs
@@ -0,0 +1,27 @@
+using RMuseum.Models.Ganjoor;
+using RSecurityBackend.Models.Generic;
+using System.Threading.Tasks;
+
+namespace RMuseum.Services
+{
+ ///
+ /// 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)
+ ///
+ public interface IGanjoorRelatedPersonService
+ {
+ ///
+ /// get all people (for the search-as-you-type person picker)
+ ///
+ ///
+ Task> GetPeopleAsync();
+
+ ///
+ /// get person by id
+ ///
+ ///
+ ///
+ Task> GetPersonAsync(int id);
+ }
+}
diff --git a/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs b/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs
new file mode 100644
index 00000000..b4d42e1f
--- /dev/null
+++ b/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs
@@ -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
+{
+ ///
+ /// related people service implementation
+ ///
+ public class GanjoorRelatedPersonService : IGanjoorRelatedPersonService
+ {
+ ///
+ /// get all people
+ ///
+ ///
+ public async Task> GetPeopleAsync()
+ {
+ try
+ {
+ return new RServiceResult
+ (
+ await _context.GanjoorRelatedPersons
+ .OrderBy(p => p.Name).ToArrayAsync()
+ );
+ }
+ catch (Exception exp)
+ {
+ return new RServiceResult(null, exp.ToString());
+ }
+ }
+
+ ///
+ /// get person by id
+ ///
+ ///
+ ///
+ public async Task> GetPersonAsync(int id)
+ {
+ try
+ {
+ return new RServiceResult
+ (
+ await _context.GanjoorRelatedPersons
+ .Where(p => p.Id == id)
+ .SingleOrDefaultAsync()
+ );
+ }
+ catch (Exception exp)
+ {
+ return new RServiceResult(null, exp.ToString());
+ }
+ }
+
+ ///
+ /// Database Context
+ ///
+ protected readonly RMuseumDbContext _context;
+
+ ///
+ /// constructor
+ ///
+ ///
+ public GanjoorRelatedPersonService(RMuseumDbContext context)
+ {
+ _context = context;
+ }
+ }
+}
diff --git a/RMuseum/Startup.cs b/RMuseum/Startup.cs
index 7796e19b..8987f8d9 100644
--- a/RMuseum/Startup.cs
+++ b/RMuseum/Startup.cs
@@ -300,6 +300,9 @@ namespace RMuseum
//geo location service
services.AddTransient();
+ //related people (family tree / person tagging) service
+ services.AddTransient();
+
//tracking service
services.AddTransient();