From b1edce7fbd439a994d00f295c3f6decfe081f828 Mon Sep 17 00:00:00 2001 From: Robert Brands Date: Sat, 16 Sep 2023 13:22:52 +0200 Subject: [PATCH] Co-Guide Feature (#129) * Data fields defined * First working version with Co-Guides * Co-Guide Feature --------- Co-authored-by: Robert Brands (RiwaAdmin) --- MeetUpFunctions/AddCoGuide.cs | 138 ++++++++++++++++++ MeetUpFunctions/Constants.cs | 2 +- .../MeetUpPlanner.Functions.csproj | 2 +- MeetUpPlanner/Client/Pages/About.razor | 2 +- MeetUpPlanner/Client/Pages/Calendar.razor | 55 +++++++ MeetUpPlanner/Client/Pages/NewMeetUp.razor | 12 ++ .../Client/Shared/ParticipantsList.razor | 14 +- .../Server/Controllers/CalendarController.cs | 11 ++ .../Server/Controllers/UtilController.cs | 2 +- .../Server/Repositories/MeetUpFunctions.cs | 10 ++ MeetUpPlanner/Shared/CalendarItem.cs | 2 + MeetUpPlanner/Shared/ExtendedCalendarItem.cs | 24 +++ MeetUpPlanner/Shared/Participant.cs | 26 ++++ 13 files changed, 292 insertions(+), 8 deletions(-) create mode 100644 MeetUpFunctions/AddCoGuide.cs diff --git a/MeetUpFunctions/AddCoGuide.cs b/MeetUpFunctions/AddCoGuide.cs new file mode 100644 index 0000000..a4ae718 --- /dev/null +++ b/MeetUpFunctions/AddCoGuide.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Web.Http; +using Aliencube.AzureFunctions.Extensions.OpenApi.Core.Attributes; +using MeetUpPlanner.Shared; +using System.Collections.Generic; + +namespace MeetUpPlanner.Functions +{ + public class AddCoGuide + { + private readonly ILogger _logger; + private ServerSettingsRepository _serverSettingsRepository; + private CosmosDBRepository _cosmosRepository; + private CosmosDBRepository _calendarRepository; + public AddCoGuide(ILogger logger, + ServerSettingsRepository serverSettingsRepository, + CosmosDBRepository cosmosRepository, + CosmosDBRepository calendarRepository) + { + _logger = logger; + _serverSettingsRepository = serverSettingsRepository; + _cosmosRepository = cosmosRepository; + _calendarRepository = calendarRepository; + } + + [FunctionName("AddCoGuide")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req) + { + _logger.LogInformation($"C# HTTP trigger function AddParticipantToCalendarItem processed a request."); + string tenant = req.Headers[Constants.HEADER_TENANT]; + if (String.IsNullOrWhiteSpace(tenant)) + { + tenant = null; + } + ServerSettings serverSettings = await _serverSettingsRepository.GetServerSettings(tenant); + + string keyWord = req.Headers[Constants.HEADER_KEYWORD]; + if (String.IsNullOrEmpty(keyWord) || !(serverSettings.IsUser(keyWord) || _serverSettingsRepository.IsInvitedGuest(keyWord))) + { + return new BadRequestErrorMessageResult("Keyword is missing or wrong."); + } + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + Participant participant = JsonConvert.DeserializeObject(requestBody); + // Get and check corresponding CalendarItem + if (String.IsNullOrEmpty(participant.CalendarItemId)) + { + return new OkObjectResult(new BackendResult(false, "Terminangabe fehlt.")); + } + CalendarItem calendarItem = await _calendarRepository.GetItem(participant.CalendarItemId); + if (null == calendarItem) + { + return new OkObjectResult(new BackendResult(false, "Angegebenen Termin nicht gefunden.")); + } + // Get participant list to check max registrations and if caller is already registered. + IEnumerable participants = await _cosmosRepository.GetItems(p => p.CalendarItemId.Equals(calendarItem.Id)); + int counter = calendarItem.WithoutHost ? 0 : 1; + int waitingCounter = 0; + int coGuideCounter = 0; + bool alreadyRegistered = false; + foreach (Participant p in participants) + { + if (p.ParticipantFirstName.Equals(participant.ParticipantFirstName) && p.ParticipantLastName.Equals(participant.ParticipantLastName)) + { + // already registered + alreadyRegistered = true; + participant.Id = p.Id; + } + if (!p.IsWaiting) + { + ++counter; + } + else + { + ++waitingCounter; + } + if (p.IsCoGuide) + { + ++coGuideCounter; + } + } + int maxRegistrationCount = calendarItem.MaxRegistrationsCount; + if (serverSettings.IsAdmin(keyWord)) + { + // Admin can "overbook" a meetup to be able to add some extra guests + maxRegistrationCount *= Constants.ADMINOVERBOOKFACTOR; + } + // Add extra slots for guides + if (coGuideCounter < calendarItem.MaxCoGuidesCount) + { + maxRegistrationCount += (calendarItem.MaxCoGuidesCount - coGuideCounter); + } + if (!alreadyRegistered) + { + if (counter < maxRegistrationCount) + { + ++counter; + participant.IsWaiting = false; + } + else if (waitingCounter < calendarItem.MaxWaitingList) + { + ++waitingCounter; + participant.IsWaiting = true; + } + else + { + return new OkObjectResult(new BackendResult(false, "Maximale Anzahl Registrierungen bereits erreicht.")); + } + } + // Set TTL for participant the same as for CalendarItem + System.TimeSpan diffTime = calendarItem.StartDate.Subtract(DateTime.Now); + participant.TimeToLive = serverSettings.AutoDeleteAfterDays * 24 * 3600 + (int)diffTime.TotalSeconds; + // Checkindate to track bookings + participant.CheckInDate = DateTime.Now; + // Set CoGuide flag + participant.IsCoGuide = true; + if (null != tenant) + { + participant.Tenant = tenant; + } + participant.Federation = serverSettings.Federation; + + participant = await _cosmosRepository.UpsertItem(participant); + BackendResult result = new BackendResult(true); + + return new OkObjectResult(result); + + } + } +} diff --git a/MeetUpFunctions/Constants.cs b/MeetUpFunctions/Constants.cs index 6d45dfd..12eeddc 100644 --- a/MeetUpFunctions/Constants.cs +++ b/MeetUpFunctions/Constants.cs @@ -22,7 +22,7 @@ public static class Constants public const string DEFAULT_DISCLAIMER = "Disclaimer"; public const string DEFAULT_GUEST_DISCLAIMER = "Guest Disclaimer"; - public const string VERSION = "2023-09-15"; + public const string VERSION = "2023-09-16"; public const int ADMINOVERBOOKFACTOR = 1; // no overbooking any more, because not needed public const int LOG_TTL = 30 * 24 * 3600; // 30 days TTL for Log items diff --git a/MeetUpFunctions/MeetUpPlanner.Functions.csproj b/MeetUpFunctions/MeetUpPlanner.Functions.csproj index d202630..605cfe0 100644 --- a/MeetUpFunctions/MeetUpPlanner.Functions.csproj +++ b/MeetUpFunctions/MeetUpPlanner.Functions.csproj @@ -7,7 +7,7 @@ - + diff --git a/MeetUpPlanner/Client/Pages/About.razor b/MeetUpPlanner/Client/Pages/About.razor index 4ad2534..3a26f2f 100644 --- a/MeetUpPlanner/Client/Pages/About.razor +++ b/MeetUpPlanner/Client/Pages/About.razor @@ -51,7 +51,7 @@ @code { - private const string clientVersion = "2023-09-15"; + private const string clientVersion = "2023-09-16"; private string serverVersion = "tbd"; private string functionsVersion = "tbd"; diff --git a/MeetUpPlanner/Client/Pages/Calendar.razor b/MeetUpPlanner/Client/Pages/Calendar.razor index 53b3b18..a476e32 100644 --- a/MeetUpPlanner/Client/Pages/Calendar.razor +++ b/MeetUpPlanner/Client/Pages/Calendar.razor @@ -215,6 +215,7 @@ @if (!CheckIfUserIsHostWithoutKeyword(item) && !CheckIfUserIsHost(item) && !ShowHistory()) { + } @@ -467,6 +468,51 @@ } checkInDisabled = false; } + protected async Task CheckinAsCoGuide(string itemId) + { + Participant participant = new Participant(); + participant.ParticipantFirstName = AppStateStore.FirstName; + participant.ParticipantLastName = AppStateStore.LastName; + participant.ParticipantAdressInfo = AppStateStore.PhoneMail; + participant.CalendarItemId = itemId; + participant.IsCoGuide = true; + checkInDisabled = true; + StateHasChanged(); + PrepareHttpClient(); + HttpResponseMessage response = await Http.PostAsJsonAsync($"Calendar/addparticipantascoguide", participant); + string responseBody = await response.Content.ReadAsStringAsync(); + + BackendResult result = JsonConvert.DeserializeObject(responseBody); + if (result.Success) + { + if (IsConnected) await SendMessage(); + // Read data again + await ReadData(); + foreach (ExtendedCalendarItem c in calendarItems) + { + if (c.Id.Equals(itemId)) + { + participant = c.FindParticipant(participant.ParticipantFirstName, participant.ParticipantLastName); + break; + } + } + if (null != participant && participant.IsWaiting) + { + notificationService.Notify(new NotificationMessage() { Severity = NotificationSeverity.Warning, Summary = "Warteliste", Detail = "Du stehst jetzt auf der Warteliste. Falls du doch nicht dabei sein kannst, melde dich bitte wieder ab.", Duration = 4000 }); + } + else + { + notificationService.Notify(new NotificationMessage() { Severity = NotificationSeverity.Success, Summary = "Angemeldet", Detail = "Du bist jetzt angemeldet. Falls du doch nicht dabei sein kannst, melde dich bitte wieder ab.", Duration = 4000 }); + } + // Read data again + await ReadData(); + } + else + { + notificationService.Notify(new NotificationMessage() { Severity = NotificationSeverity.Error, Summary = "Fehler", Detail = result.Message, Duration = 4000 }); + } + checkInDisabled = false; + } protected async Task Checkout(ExtendedCalendarItem calendarItem) { // Find corresponding participant item @@ -492,6 +538,10 @@ bool alreadyRegistered = calendarItem.FindParticipant(AppStateStore.FirstName, AppStateStore.LastName) != null; return alreadyRegistered; } + protected bool CheckIfCoGuideIsWanted(ExtendedCalendarItem calendarItem) + { + return (KeywordCheck.IsUser && calendarItem.MaxCoGuidesCount > 0); + } protected bool CheckIfUserIsHost(ExtendedCalendarItem calendarItem) { bool isHost = !calendarItem.WithoutHost && KeywordCheck.IsUser && (calendarItem.HostFirstName.Equals(AppStateStore.FirstName) && calendarItem.HostLastName.Equals(AppStateStore.LastName)); @@ -608,6 +658,11 @@ } return checkInLabel; } + private string GetCheckInAsCoGuideLabel(ExtendedCalendarItem calendarItem) + { + string checkInLabel = "als Co-Guide"; + return checkInLabel; + } private string GetScopeLink(ExtendedCalendarItem calendarItem) { return $"{Http.BaseAddress}{calendarItem.GuestScope}"; diff --git a/MeetUpPlanner/Client/Pages/NewMeetUp.razor b/MeetUpPlanner/Client/Pages/NewMeetUp.razor index 9eaa0db..f999fd4 100644 --- a/MeetUpPlanner/Client/Pages/NewMeetUp.razor +++ b/MeetUpPlanner/Client/Pages/NewMeetUp.razor @@ -302,6 +302,18 @@ Optional: Gewünschte Mindestteilnehmerzahl der Gruppe. Dies wird entsprechend angezeigt, hat aber ansonsten keine Konsequenzen, d.h. es wird kein Termin automatisch abgesagt oder gelöscht ... +
+ + + + + + + + + Wünscht du dir Unterstützung von Co-Guides? Du kannst bis zu drei Co-Guides anfragen. + +
diff --git a/MeetUpPlanner/Client/Shared/ParticipantsList.razor b/MeetUpPlanner/Client/Shared/ParticipantsList.razor index 636130c..1791f69 100644 --- a/MeetUpPlanner/Client/Shared/ParticipantsList.razor +++ b/MeetUpPlanner/Client/Shared/ParticipantsList.razor @@ -18,11 +18,11 @@ @if (p.Federation == AppStateStore.Tenant.FederationKey) { - @p.ParticipantDisplayName(NameDisplayLength) - @p.ParticipantAdressInfo + @p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength) - @p.ParticipantAdressInfo } else { - @p.ParticipantDisplayName(NameDisplayLength)@p.Federation - @p.ParticipantAdressInfo + @p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength)@p.Federation - @p.ParticipantAdressInfo } } @@ -40,11 +40,17 @@ @if (p.Federation == AppStateStore.Tenant.FederationKey) { - @p.ParticipantDisplayName(NameDisplayLength) - @p.ParticipantAdressInfo + @p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength) - @p.ParticipantAdressInfo } else { - @p.ParticipantDisplayName(NameDisplayLength)@p.Federation - @p.ParticipantAdressInfo + @p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength) + + @p.Federation + + - + + @p.ParticipantAdressInfo } } diff --git a/MeetUpPlanner/Server/Controllers/CalendarController.cs b/MeetUpPlanner/Server/Controllers/CalendarController.cs index dcaa78a..4e70f83 100644 --- a/MeetUpPlanner/Server/Controllers/CalendarController.cs +++ b/MeetUpPlanner/Server/Controllers/CalendarController.cs @@ -124,6 +124,17 @@ public async Task AddParticipant([FromHeader(Name = "x-meetup-ten BackendResult result = await _meetUpFunctions.AddParticipantToCalendarItem(tenant, keyword, participant); return Ok(result); } + [HttpPost("addparticipantascoguide")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task AddParticipantAsCoGuide([FromHeader(Name = "x-meetup-tenant")] string tenant, [FromHeader(Name = "x-meetup-keyword")] string keyword, [FromBody] Participant participant) + { + if (String.IsNullOrEmpty(keyword)) + { + keyword = _meetUpFunctions.InviteGuestKey; + } + BackendResult result = await _meetUpFunctions.AddParticipantAsCoGuideToCalendarItem(tenant, keyword, participant); + return Ok(result); + } [HttpPost("addguest")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task AddGuest([FromHeader(Name = "x-meetup-tenant")] string tenant, [FromBody] Participant participant) diff --git a/MeetUpPlanner/Server/Controllers/UtilController.cs b/MeetUpPlanner/Server/Controllers/UtilController.cs index 208d7a3..f10b9a4 100644 --- a/MeetUpPlanner/Server/Controllers/UtilController.cs +++ b/MeetUpPlanner/Server/Controllers/UtilController.cs @@ -20,7 +20,7 @@ public class UtilController : ControllerBase { private readonly MeetUpFunctions _meetUpFunctions; private readonly ILogger logger; - const string serverVersion = "2023-09-15"; + const string serverVersion = "2023-09-16"; string functionsVersion = "tbd"; public UtilController(ILogger logger, MeetUpFunctions meetUpFunctions) diff --git a/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs b/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs index ced2bc5..8444301 100644 --- a/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs +++ b/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs @@ -315,6 +315,16 @@ public async Task AddParticipantToCalendarItem(string tenant, str .ReceiveJson(); return result; } + public async Task AddParticipantAsCoGuideToCalendarItem(string tenant, string keyword, Participant participant) + { + BackendResult result = await $"https://{_functionsConfig.FunctionAppName}.azurewebsites.net/api/AddCoGuide" + .WithHeader(HEADER_FUNCTIONS_KEY, _functionsConfig.ApiKey) + .WithHeader(HEADER_KEYWORD, keyword) + .WithHeader(HEADER_TENANT, tenant) + .PostJsonAsync(participant) + .ReceiveJson(); + return result; + } public async Task AddCommentToCalendarItem(string tenant, string keyword, CalendarComment comment) { BackendResult result; diff --git a/MeetUpPlanner/Shared/CalendarItem.cs b/MeetUpPlanner/Shared/CalendarItem.cs index 219b1e6..eeb3daa 100644 --- a/MeetUpPlanner/Shared/CalendarItem.cs +++ b/MeetUpPlanner/Shared/CalendarItem.cs @@ -39,6 +39,8 @@ public class CalendarItem : CosmosDBEntity public int MinRegistrationsCount { get; set; } = 0; [JsonProperty(PropertyName = "maxWaitingList", NullValueHandling = NullValueHandling.Ignore), Range(0.0, 150.0, ErrorMessage = "Größe der Warteliste nicht im gültigen Bereich."), Display(Name = "Maximale Anzahl auf Warteliste", Prompt = "Anzahl eingeben"), Required(ErrorMessage = "Max. Anzahl auf Warteliste eingeben")] public int MaxWaitingList { get; set; } = 0; + [JsonProperty(PropertyName = "maxCoGuidesCount", NullValueHandling = NullValueHandling.Ignore)] + public int MaxCoGuidesCount { get; set; } = 0; [JsonProperty(PropertyName = "privateKeyword", NullValueHandling = NullValueHandling.Ignore), MaxLength(50, ErrorMessage = "Privates Schlüsselwort zu lang.")] public string PrivateKeyword { get; set; } [JsonProperty(PropertyName = "isInternal")] diff --git a/MeetUpPlanner/Shared/ExtendedCalendarItem.cs b/MeetUpPlanner/Shared/ExtendedCalendarItem.cs index 9f578b6..175d187 100644 --- a/MeetUpPlanner/Shared/ExtendedCalendarItem.cs +++ b/MeetUpPlanner/Shared/ExtendedCalendarItem.cs @@ -42,6 +42,7 @@ public ExtendedCalendarItem(CalendarItem calendarItem) this.MaxRegistrationsCount = calendarItem.MaxRegistrationsCount; this.MinRegistrationsCount = calendarItem.MinRegistrationsCount; this.MaxWaitingList = calendarItem.MaxWaitingList; + this.MaxCoGuidesCount = calendarItem.MaxCoGuidesCount; this.PrivateKeyword = calendarItem.PrivateKeyword; this.IsInternal = calendarItem.IsInternal; this.LevelDescription = calendarItem.LevelDescription; @@ -61,6 +62,7 @@ public ExtendedCalendarItem(CalendarItem calendarItem) this.AttachedInfoKey = calendarItem.AttachedInfoKey; this.Federation = calendarItem.Federation; this.FederatedFrom = calendarItem.FederatedFrom; + this.MaxCoGuidesCount = calendarItem.MaxCoGuidesCount; } @@ -68,8 +70,10 @@ public string ParticipantsDisplay(int nameDisplayLength) { StringBuilder sb = new StringBuilder(100); int counter = WithoutHost ? 0 : 1; + int coGuideCounter = 0; foreach (Participant participant in this.ParticipantsList) { + if (participant.IsCoGuide) coGuideCounter++; if (!participant.IsWaiting) { if (counter > 0) @@ -77,6 +81,10 @@ public string ParticipantsDisplay(int nameDisplayLength) sb.Append(", "); } sb.Append(participant.ParticipantDisplayName(nameDisplayLength)); + if (participant.IsCoGuide && coGuideCounter <= this.MaxCoGuidesCount) + { + sb.Append("(Co-Guide)"); + } ++counter; } } @@ -169,6 +177,22 @@ public int WaitingListCounter return counter; } } + [JsonIgnore] + public int CoGuidesCounter + { + get + { + int counter = 0; + foreach (Participant p in ParticipantsList) + { + if (p.IsCoGuide) + { + ++counter; + } + } + return counter; + } + } /// /// Helper function to look for given person in the participants list /// diff --git a/MeetUpPlanner/Shared/Participant.cs b/MeetUpPlanner/Shared/Participant.cs index 37531f9..66dc09b 100644 --- a/MeetUpPlanner/Shared/Participant.cs +++ b/MeetUpPlanner/Shared/Participant.cs @@ -26,6 +26,8 @@ public class Participant : CosmosDBEntity public Boolean IsGuest { get; set; } = false; [JsonProperty(PropertyName = "isWaiting")] public Boolean IsWaiting { get; set; } = false; + [JsonProperty(PropertyName = "isCoGuide")] + public Boolean IsCoGuide { get; set; } = false; [JsonProperty(PropertyName = "federaton")] public string Federation { get; set; } @@ -59,6 +61,30 @@ public string ParticipantDisplayName(int nameDisplayLength) return sb.ToString(); } + public string ParticipantDisplayNameWithCoGuideSuffix(int nameDisplayLength) + { + StringBuilder sb = new StringBuilder(); + if (IsGuest) + { + sb.Append("Gast"); + } + else + { + int length = nameDisplayLength > 0 ? Math.Min(nameDisplayLength, ParticipantLastName.Length) : ParticipantLastName.Length; + sb.Append(ParticipantFirstName).Append(" "); + sb.Append(ParticipantLastName.Substring(0, length)); + if (length < ParticipantLastName.Length) + { + sb.Append('.'); + } + if (IsCoGuide) + { + sb.Append(" (Co-Guide)"); + } + } + + return sb.ToString(); + } } }