From 073222ec6d1e04227d0ae450783634c0ff025504 Mon Sep 17 00:00:00 2001 From: Hamid Reza Mohammadi Date: Mon, 28 Sep 2026 13:48:43 +0330 Subject: [PATCH] people tags #387 --- GanjooRazor/Areas/User/Pages/Editor.cshtml | 359 +++++++++++++++++- GanjooRazor/Areas/User/Pages/Editor.cshtml.cs | 25 ++ GanjooRazor/wwwroot/js/bk.js | 97 +++++ .../GanjoorRelatedPersonController.cs | 64 ++++ .../Services/IGanjoorRelatedPersonService.cs | 27 ++ .../GanjoorRelatedPersonService.cs | 72 ++++ RMuseum/Startup.cs | 3 + 7 files changed, 643 insertions(+), 4 deletions(-) create mode 100644 RMuseum/Controllers/GanjoorRelatedPersonController.cs create mode 100644 RMuseum/Services/IGanjoorRelatedPersonService.cs create mode 100644 RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs 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 += '' + + 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 += '' + ''; } html += '
وضعیتبیتمکانفردسالماهروز#
' + status + '' + coupletText + '' + locationText + '' + (s.lunarYear || '') + '' + (s.lunarMonth || '') + '' + (s.lunarDay || '') + '
' + status + '' + coupletText + '' + locationText + '' + personText + '' + (s.lunarYear || '') + '' + (s.lunarMonth || '') + '' + (s.lunarDay || '') + '✕ حذف از فهرست
'; @@ -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();