diff --git a/MeetUpFunctions/AddParticipantToCalendarItem.cs b/MeetUpFunctions/AddParticipantToCalendarItem.cs index d0f15b7..7e8846c 100644 --- a/MeetUpFunctions/AddParticipantToCalendarItem.cs +++ b/MeetUpFunctions/AddParticipantToCalendarItem.cs @@ -70,6 +70,12 @@ public async Task Run( } ++counter; } + int maxRegistrationCount = calendarItem.MaxRegistrationsCount; + if (serverSettings.IsAdmin(keyWord)) + { + // Admin can "overbook" a meetup to be able to add some extra guests + maxRegistrationCount *= Constants.ADMINOVERBOOKFACTOR; + } if (counter >= calendarItem.MaxRegistrationsCount) { return new OkObjectResult(new BackendResult(false, "Maximale Anzahl Registrierungen bereits erreicht.")); diff --git a/MeetUpFunctions/Constants.cs b/MeetUpFunctions/Constants.cs index 087f74e..0df4e2a 100644 --- a/MeetUpFunctions/Constants.cs +++ b/MeetUpFunctions/Constants.cs @@ -18,6 +18,9 @@ public static class Constants public const string DEFAULT_LINK = "https://robert-brands.com"; public const string DEFAULT_LINK_TITLE = "https://robert-brands.com"; - public const string VERSION = "2020-07-28"; + public const string VERSION = "2020-08-02"; + public const int ADMINOVERBOOKFACTOR = 2; + + public const int LOG_TTL = 30 * 24 * 3600; // 30 days TTL for Log items } } diff --git a/MeetUpFunctions/ExportTrackingReport.cs b/MeetUpFunctions/ExportTrackingReport.cs new file mode 100644 index 0000000..48a7610 --- /dev/null +++ b/MeetUpFunctions/ExportTrackingReport.cs @@ -0,0 +1,118 @@ +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 System.Collections.Generic; +using Newtonsoft.Json; +using MeetUpPlanner.Shared; +using System.Web.Http; +using System.Linq; +using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; +using System.Collections; + +namespace MeetUpPlanner.Functions +{ + public class ExportTrackingReport + { + private readonly ILogger _logger; + private ServerSettingsRepository _serverSettingsRepository; + private CosmosDBRepository _cosmosRepository; + private CosmosDBRepository _participantRepository; + private CosmosDBRepository _logRepository; + + public ExportTrackingReport(ILogger logger, + ServerSettingsRepository serverSettingsRepository, + CosmosDBRepository cosmosRepository, + CosmosDBRepository participantRepository, + CosmosDBRepository logRepository) + { + _logger = logger; + _serverSettingsRepository = serverSettingsRepository; + _cosmosRepository = cosmosRepository; + _participantRepository = participantRepository; + _logRepository = logRepository; + } + + [FunctionName("ExportTrackingReport")] + [OpenApiOperation(Summary = "Export a list of participants of the given user sharing rides", + Description = "All CalendarItems still in database are scanned for participants who had shared an envent with the given person. To be able to read all ExtendedCalenderItems the admin keyword must be set as header x-meetup-keyword.")] + [OpenApiRequestBody("application/json", typeof(TrackingReportRequest), Description = "Holds all information needed to assemble a tracking report")] + [OpenApiResponseBody(System.Net.HttpStatusCode.OK, "application/json", typeof(TrackingReport))] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req) + { + _logger.LogInformation("C# HTTP trigger function ExportTrackingReport processed a request."); + ServerSettings serverSettings = await _serverSettingsRepository.GetServerSettings(); + + string keyWord = req.Headers[Constants.HEADER_KEYWORD]; + if (String.IsNullOrEmpty(keyWord) || !serverSettings.IsAdmin(keyWord)) + { + _logger.LogWarning("ExportTrackingReport called with wrong keyword."); + return new BadRequestErrorMessageResult("Keyword is missing or wrong."); + } + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + TrackingReportRequest trackingRequest = JsonConvert.DeserializeObject(requestBody); + if (String.IsNullOrEmpty(trackingRequest.RequestorFirstName) || String.IsNullOrEmpty(trackingRequest.RequestorLastName)) + { + _logger.LogWarning("ExportTrackingReport called without name of requestor."); + return new BadRequestErrorMessageResult("Requestor name missing."); + } + if (String.IsNullOrEmpty(trackingRequest.TrackFirstName) || String.IsNullOrEmpty(trackingRequest.TrackLastName)) + { + _logger.LogWarning("ExportTrackingReport called without name of person to track."); + return new BadRequestErrorMessageResult("Track name missing."); + } + // Get a list of all CalendarItems + IEnumerable rawListOfCalendarItems = await _cosmosRepository.GetItems(); + List resultCalendarItems = new List(50); + // Filter the CalendarItems that are relevant + foreach (CalendarItem item in rawListOfCalendarItems) + { + // Read all participants for this calendar item + IEnumerable participants = await _participantRepository.GetItems(p => p.CalendarItemId.Equals(item.Id)); + // Only events where the person was part of will be used. + if (item.EqualsHost(trackingRequest.TrackFirstName, trackingRequest.TrackLastName) || null != participants.Find(trackingRequest.TrackFirstName, trackingRequest.TrackLastName)) + { + ExtendedCalendarItem extendedItem = new ExtendedCalendarItem(item); + extendedItem.ParticipantsList = participants; + resultCalendarItems.Add(extendedItem); + } + + } + IEnumerable orderedList = resultCalendarItems.OrderBy(d => d.StartDate); + // Build template for marker list corresponding to orderedList above + List relevantCalendarList = new List(50); + int calendarSize = 0; + foreach (ExtendedCalendarItem e in orderedList) + { + relevantCalendarList.Add(new CompanionCalendarInfo(e)); + ++calendarSize; + } + + // Assemble report + TrackingReport report = new TrackingReport(trackingRequest); + report.CompanionList = new List(50); + report.CalendarList = relevantCalendarList; + int calendarIndex = 0; + foreach (ExtendedCalendarItem calendarItem in orderedList) + { + report.CompanionList.AddCompanion(calendarItem.HostFirstName, calendarItem.HostLastName, calendarItem.HostAdressInfo, calendarSize, calendarIndex); + foreach (Participant p in calendarItem.ParticipantsList) + { + report.CompanionList.AddCompanion(p.ParticipantFirstName, p.ParticipantLastName, p.ParticipantAdressInfo, calendarSize, calendarIndex); + } + ++calendarIndex; + } + report.CreationDate = DateTime.Now; + ExportLogItem log = new ExportLogItem(trackingRequest); + log.TimeToLive = Constants.LOG_TTL; + await _logRepository.CreateItem(log); + + return new OkObjectResult(report); + } + } +} diff --git a/MeetUpFunctions/Extensions.cs b/MeetUpFunctions/Extensions.cs new file mode 100644 index 0000000..27c5a1e --- /dev/null +++ b/MeetUpFunctions/Extensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MeetUpPlanner.Shared; + +namespace MeetUpPlanner.Functions +{ + public static class Extensions + { + /// + /// Extension method to find a given person with given firstName and lastName. Case is ignored for comparison. + /// + /// + /// + /// + /// + public static Participant Find(this IEnumerable participantList, string firstName, string lastName) + { + Participant participant = null; + foreach (Participant p in participantList) + { + if (p.ParticipantFirstName.Equals(firstName, StringComparison.InvariantCultureIgnoreCase) && p.ParticipantLastName.Equals(lastName, StringComparison.InvariantCultureIgnoreCase)) + { + participant = p; + break; + } + } + return participant; + } + public static void AddCompanion(this IList companionList, string firstName, string lastName, string addressInfo, int sizeOfCalendarList, int calendarIndex) + { + Companion companion = companionList.FirstOrDefault(c => c.FirstName.Equals(firstName, StringComparison.InvariantCultureIgnoreCase) + && c.LastName.Equals(lastName, StringComparison.InvariantCultureIgnoreCase)); + if (null == companion) + { + companion = new Companion(firstName, lastName, addressInfo, sizeOfCalendarList); + companionList.Add(companion); + } + else + { + // Add addressInfo to get the latest info. + companion.AddressInfo = addressInfo; + } + companion.EventList[calendarIndex] = true; + } + + public static bool EqualsHost(this CalendarItem calendarItem, string firstName, string lastName) + { + return (calendarItem.HostFirstName.Equals(firstName, StringComparison.InvariantCultureIgnoreCase) && calendarItem.HostLastName.Equals(lastName, StringComparison.InvariantCultureIgnoreCase)); + } + } +} diff --git a/MeetUpFunctions/GetAllExtendedCalendarItems.cs b/MeetUpFunctions/GetAllExtendedCalendarItems.cs new file mode 100644 index 0000000..fb50728 --- /dev/null +++ b/MeetUpFunctions/GetAllExtendedCalendarItems.cs @@ -0,0 +1,71 @@ +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 System.Collections.Generic; +using Newtonsoft.Json; +using MeetUpPlanner.Shared; +using System.Web.Http; +using System.Linq; +using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; + +namespace MeetUpPlanner.Functions +{ + public class GetAllExtendedCalendarItems + { + private readonly ILogger _logger; + private ServerSettingsRepository _serverSettingsRepository; + private CosmosDBRepository _cosmosRepository; + private CosmosDBRepository _participantRepository; + private CosmosDBRepository _commentRepository; + + public GetAllExtendedCalendarItems(ILogger logger, ServerSettingsRepository serverSettingsRepository, + CosmosDBRepository cosmosRepository, + CosmosDBRepository participantRepository, + CosmosDBRepository commentRepository) + { + _logger = logger; + _serverSettingsRepository = serverSettingsRepository; + _cosmosRepository = cosmosRepository; + _participantRepository = participantRepository; + _commentRepository = commentRepository; + } + + [FunctionName("GetAllExtendedCalendarItems")] + [OpenApiOperation(Summary = "Gets all ExtendedCalendarIitems", + Description = "Reading all ExtendedCalendarItems (including particpants and comments) for tracking issues. To be able to read all ExtendedCalenderItems the admin keyword must be set as header x-meetup-keyword.")] + [OpenApiResponseBody(System.Net.HttpStatusCode.OK, "application/json", typeof(IEnumerable))] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req) + { + _logger.LogInformation("C# HTTP trigger function GetAllExtendedCalendarItems processed a request."); + ServerSettings serverSettings = await _serverSettingsRepository.GetServerSettings(); + + string keyWord = req.Headers[Constants.HEADER_KEYWORD]; + if (String.IsNullOrEmpty(keyWord) || !serverSettings.IsAdmin(keyWord)) + { + _logger.LogWarning("GetAllExtendedCalendarItems called with wrong keyword."); + return new BadRequestErrorMessageResult("Keyword is missing or wrong."); + } + // Get a list of all CalendarItems + + IEnumerable rawListOfCalendarItems = await _cosmosRepository.GetItems(); + List resultCalendarItems = new List(50); + foreach (CalendarItem item in rawListOfCalendarItems) + { + ExtendedCalendarItem extendedItem = new ExtendedCalendarItem(item); + resultCalendarItems.Add(extendedItem); + // Read all participants for this calendar item + extendedItem.ParticipantsList = await _participantRepository.GetItems(p => p.CalendarItemId.Equals(extendedItem.Id)); + // Read all comments + extendedItem.CommentsList = await _commentRepository.GetItems(c => c.CalendarItemId.Equals(extendedItem.Id)); + } + IEnumerable orderedList = resultCalendarItems.OrderBy(d => d.StartDate); + return new OkObjectResult(orderedList); + } + } +} diff --git a/MeetUpFunctions/GetExportLog.cs b/MeetUpFunctions/GetExportLog.cs new file mode 100644 index 0000000..daed757 --- /dev/null +++ b/MeetUpFunctions/GetExportLog.cs @@ -0,0 +1,54 @@ +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 System.Collections.Generic; +using Newtonsoft.Json; +using MeetUpPlanner.Shared; +using System.Web.Http; +using System.Linq; +using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; + +namespace MeetUpPlanner.Functions +{ + public class GetExportLog + { + private readonly ILogger _logger; + private ServerSettingsRepository _serverSettingsRepository; + private CosmosDBRepository _cosmosRepository; + + public GetExportLog(ILogger logger, ServerSettingsRepository serverSettingsRepository, CosmosDBRepository cosmosRepository) + { + _logger = logger; + _serverSettingsRepository = serverSettingsRepository; + _cosmosRepository = cosmosRepository; + } + + [FunctionName("GetExportLog")] + [OpenApiOperation(Summary = "Returns all export log items", + Description = "Reading all ExportLogItems. To be able to read ExportLogItems the admin keyword must be set as header x-meetup-keyword.")] + [OpenApiResponseBody(System.Net.HttpStatusCode.OK, "application/json", typeof(IEnumerable))] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req) + { + _logger.LogInformation("C# HTTP trigger function GetExportLog processed a request."); + ServerSettings serverSettings = await _serverSettingsRepository.GetServerSettings(); + + string keyWord = req.Headers[Constants.HEADER_KEYWORD]; + if (String.IsNullOrEmpty(keyWord) || !serverSettings.IsAdmin(keyWord)) + { + _logger.LogWarning("GetExportLog called with wrong keyword."); + return new BadRequestErrorMessageResult("Keyword is missing or wrong."); + } + + + IEnumerable exportLog = await _cosmosRepository.GetItems(); + IEnumerable orderedList = exportLog.OrderByDescending(l => l.RequestDate); + return new OkObjectResult(orderedList); + } + } +} diff --git a/MeetUpFunctions/GetExtendedCalendarItems.cs b/MeetUpFunctions/GetExtendedCalendarItems.cs index 0e07ff7..83d7580 100644 --- a/MeetUpFunctions/GetExtendedCalendarItems.cs +++ b/MeetUpFunctions/GetExtendedCalendarItems.cs @@ -36,8 +36,8 @@ public GetExtendedCalendarItems(ILogger logger, Server } [FunctionName("GetExtendedCalendarItems")] - [OpenApiOperation(Summary = "Gets the relevant CalendarIitems", - Description = "Reading current CalendarItems starting in the future or the configured past (in hours). To be able to read CalenderItems the user keyword must be set as header x-meetup-keyword.")] + [OpenApiOperation(Summary = "Gets the relevant ExtendedCalendarIitems", + Description = "Reading current ExtendedCalendarItems (CalendarItem including correpondent participants and comments) starting in the future or the configured past (in hours). To be able to read CalenderItems the user keyword must be set as header x-meetup-keyword.")] [OpenApiResponseBody(System.Net.HttpStatusCode.OK, "application/json", typeof(IEnumerable))] [OpenApiParameter("privatekeywords", Description = "Holds a list of private keywords, separated by ;")] public async Task Run( diff --git a/MeetUpFunctions/Startup.cs b/MeetUpFunctions/Startup.cs index ae9ca3c..58c0a0b 100644 --- a/MeetUpFunctions/Startup.cs +++ b/MeetUpFunctions/Startup.cs @@ -42,6 +42,7 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddSingleton(new CosmosDBRepository(config, cosmosClient)); builder.Services.AddSingleton(new CosmosDBRepository(config, cosmosClient)); builder.Services.AddSingleton(new CosmosDBRepository(config, cosmosClient)); + builder.Services.AddSingleton(new CosmosDBRepository(config, cosmosClient)); builder.Services.AddSingleton(new ServerSettingsRepository(config, cosmosClient)); } } diff --git a/MeetUpPlanner/Client/App.razor b/MeetUpPlanner/Client/App.razor index 6f67a6e..422f33e 100644 --- a/MeetUpPlanner/Client/App.razor +++ b/MeetUpPlanner/Client/App.razor @@ -4,7 +4,7 @@ -

Sorry, there's nothing at this address.

+

Ups, aber der Link ist falsch.

diff --git a/MeetUpPlanner/Client/AppState.cs b/MeetUpPlanner/Client/AppState.cs index 8b166d6..80d56be 100644 --- a/MeetUpPlanner/Client/AppState.cs +++ b/MeetUpPlanner/Client/AppState.cs @@ -39,7 +39,7 @@ public ClientSettings ClientSettings [MaxLength(80, ErrorMessage = "Der Nachname ist zu lang."), MinLength(3, ErrorMessage = "Der Nachname ist zu kurz.")] [Required(ErrorMessage = "Der Nachname fehlt.")] public string LastName { get; set; } - [MaxLength(60, ErrorMessage = "Die Tel-Nr/Mail-Adresse ist zu lang"), MinLength(8, ErrorMessage = "Die Tel-Nr/Mail-Adresse ist zu kurz.")] + [MaxLength(120, ErrorMessage = "Die Tel-Nr/Mail-Adresse ist zu lang"), MinLength(8, ErrorMessage = "Die Tel-Nr/Mail-Adresse ist zu kurz.")] [Required(ErrorMessage = "Tel-Nr/Mail-Adresse fehlen.")] public string PhoneMail { get; set; } public bool SaveSettings { get; set; } = true; diff --git a/MeetUpPlanner/Client/BlazorTimer.cs b/MeetUpPlanner/Client/BlazorTimer.cs index 59ac2ce..d0dcb2a 100644 --- a/MeetUpPlanner/Client/BlazorTimer.cs +++ b/MeetUpPlanner/Client/BlazorTimer.cs @@ -15,7 +15,10 @@ public void SetTimer(double interval) } public void DisableTimer() { - _timer.Enabled = false; + if ( null != _timer ) + { + _timer.Enabled = false; + } } public event Action OnElapsed; diff --git a/MeetUpPlanner/Client/MeetUpPlanner.Client.csproj b/MeetUpPlanner/Client/MeetUpPlanner.Client.csproj index 4359118..702b18c 100644 --- a/MeetUpPlanner/Client/MeetUpPlanner.Client.csproj +++ b/MeetUpPlanner/Client/MeetUpPlanner.Client.csproj @@ -6,6 +6,7 @@ + diff --git a/MeetUpPlanner/Client/Pages/About.razor b/MeetUpPlanner/Client/Pages/About.razor index 20c01a6..6a9a41e 100644 --- a/MeetUpPlanner/Client/Pages/About.razor +++ b/MeetUpPlanner/Client/Pages/About.razor @@ -9,7 +9,7 @@

Diese kleine Anwendung habe ich in den "Corona"-Zeiten entwickelt, damit Radausfahrten so organisiert werden können, dass - die Teilnehmer sich registrieren können und so alle Kontakte nachvollziehbar sind. + die Teilnehmer sich registrieren können und alle Kontakte nachvollziehbar sind.

Folgende Ziele habe ich mir für die App gesetzt: @@ -33,7 +33,7 @@

Und nebenbei oder vielleicht sogar vor allem möchte ich hiermit eine neue Technik ausprobieren: Blazor ... - Die Anwendung ist übrigens als Open Source auf GitHub öffentlich zugänglich. + Die Anwendung ist übrigens als Open Source auf GitHub öffentlich zugänglich.

Versionen: Client - @clientVersion / Server - @serverVersion / Functions - @functionsVersion @@ -45,7 +45,7 @@

@code { - private const string clientVersion = "2020-07-28"; + private const string clientVersion = "2020-08-02"; private string serverVersion = "tbd"; private string functionsVersion = "tbd"; diff --git a/MeetUpPlanner/Client/Pages/Admin.razor b/MeetUpPlanner/Client/Pages/Admin.razor index 1924c15..046bc6a 100644 --- a/MeetUpPlanner/Client/Pages/Admin.razor +++ b/MeetUpPlanner/Client/Pages/Admin.razor @@ -1,6 +1,7 @@ @page "/admin" @inject HttpClient Http @inject AppState AppStateStore +@inject NavigationManager NavigationManager @using MeetUpPlanner.Shared @using Blazored.TextEditor @@ -204,12 +205,19 @@ protected override async Task OnInitializedAsync() { - string keyword = AppStateStore.KeyWord; - // Get server settings from server - serverSettings = await Http.GetFromJsonAsync($"Util/serversettings?adminKeyword={keyword}"); - await htmlWelcomeMessage.LoadHTMLContent(AppStateStore.ClientSettings.WelcomeMessage); - await htmlWhiteboardMessage.LoadHTMLContent(AppStateStore.ClientSettings.WhiteboardMessage); - await htmlNewMeetupMessage.LoadHTMLContent(AppStateStore.ClientSettings.NewMeetupMessage); + if (!String.IsNullOrEmpty(AppStateStore?.KeyWord)) + { + string keyword = AppStateStore.KeyWord; + // Get server settings from server + serverSettings = await Http.GetFromJsonAsync($"Util/serversettings?adminKeyword={keyword}"); + await htmlWelcomeMessage.LoadHTMLContent(AppStateStore.ClientSettings.WelcomeMessage); + await htmlWhiteboardMessage.LoadHTMLContent(AppStateStore.ClientSettings.WhiteboardMessage); + await htmlNewMeetupMessage.LoadHTMLContent(AppStateStore.ClientSettings.NewMeetupMessage); + } + else + { + NavigationManager.NavigateTo("/"); + } } private async Task SaveClientSettings() diff --git a/MeetUpPlanner/Client/Pages/Calendar.razor b/MeetUpPlanner/Client/Pages/Calendar.razor index 1afd3f8..afbebf2 100644 --- a/MeetUpPlanner/Client/Pages/Calendar.razor +++ b/MeetUpPlanner/Client/Pages/Calendar.razor @@ -10,17 +10,17 @@ @using Newtonsoft.Json @using System.IO -@if (!String.IsNullOrEmpty(AppStateStore.ClientSettings.WhiteboardMessage)) +@if (!String.IsNullOrEmpty(AppStateStore.ClientSettings?.WhiteboardMessage)) {
- @if (!String.IsNullOrEmpty(AppStateStore.ClientSettings.LogoLink)) + @if (!String.IsNullOrEmpty(AppStateStore.ClientSettings?.LogoLink)) {
- +
}
- @((MarkupString)AppStateStore.ClientSettings.WhiteboardMessage) + @((MarkupString)AppStateStore.ClientSettings?.WhiteboardMessage)
} @@ -46,7 +46,7 @@
- @item.GetStartDateAsString() - @item.Place + @item.GetStartDateAsString() - @item.Place @if (!String.IsNullOrEmpty(item.PrivateKeyword)) {
@item.PrivateKeyword
@@ -105,19 +105,26 @@ protected override async Task OnInitializedAsync() { - // Get list of calendar items - await ReadData(); - Timer.SetTimer(refreshInterval); - Timer.OnElapsed += TimerElapsedHandler; - Timer2.SetTimer(navigateAwayInterval); - Timer2.OnElapsed += NavigateAwayHandler; + if (null != AppStateStore && !String.IsNullOrEmpty(AppStateStore.KeyWord)) + { + // Get list of calendar items + await ReadData(); + Timer.SetTimer(refreshInterval); + Timer.OnElapsed += TimerElapsedHandler; + Timer2.SetTimer(navigateAwayInterval); + Timer2.OnElapsed += NavigateAwayHandler; + } + else + { + NavigationManager.NavigateTo("/"); + } } protected async Task ReadData() { string keyword = AppStateStore.KeyWord; string privateKeywords = AppStateStore.PrivateKeyWord1 + ";" + AppStateStore.PrivateKeyWord2 + ";" + AppStateStore.PrivateKeyWord3; ; // Get list of calendar items - calendarItems = await Http.GetFromJsonAsync>($"Calendar/extendedcalendaritems?keyword={keyword}&privatekeywords={privateKeywords}"); + calendarItems = await Http.GetFromJsonAsync>($"calendar/extendedcalendaritems?keyword={keyword}&privatekeywords={privateKeywords}"); } protected void EditCalendarItem(string itemId) @@ -130,6 +137,7 @@ Participant participant = new Participant(); participant.ParticipantFirstName = AppStateStore.FirstName; participant.ParticipantLastName = AppStateStore.LastName; + participant.ParticipantAdressInfo = AppStateStore.PhoneMail; participant.CalendarItemId = itemId; string keyword = AppStateStore.KeyWord; HttpResponseMessage response = await Http.PostAsJsonAsync($"Calendar/addparticipant?keyword={keyword}", participant); diff --git a/MeetUpPlanner/Client/Pages/Comment.razor b/MeetUpPlanner/Client/Pages/Comment.razor index 33a69f7..df9b44e 100644 --- a/MeetUpPlanner/Client/Pages/Comment.razor +++ b/MeetUpPlanner/Client/Pages/Comment.razor @@ -57,9 +57,16 @@ protected override async Task OnInitializedAsync() { - await ReadData(); - comment.AuthorFirstName = AppStateStore.FirstName; - comment.AuthorLastName = AppStateStore.LastName; + if (!String.IsNullOrEmpty(AppStateStore?.KeyWord)) + { + await ReadData(); + comment.AuthorFirstName = AppStateStore.FirstName; + comment.AuthorLastName = AppStateStore.LastName; + } + else + { + NavigationManager.NavigateTo("/"); + } } private async Task ReadData() { diff --git a/MeetUpPlanner/Client/Pages/ConfirmDelete.razor b/MeetUpPlanner/Client/Pages/ConfirmDelete.razor index 709d256..51a1137 100644 --- a/MeetUpPlanner/Client/Pages/ConfirmDelete.razor +++ b/MeetUpPlanner/Client/Pages/ConfirmDelete.razor @@ -36,7 +36,14 @@ protected override async Task OnInitializedAsync() { - await ReadData(); + if (!String.IsNullOrEmpty(AppStateStore?.KeyWord)) + { + await ReadData(); + } + else + { + NavigationManager.NavigateTo("/"); + } } private async Task ReadData() { diff --git a/MeetUpPlanner/Client/Pages/Export.razor b/MeetUpPlanner/Client/Pages/Export.razor new file mode 100644 index 0000000..878de76 --- /dev/null +++ b/MeetUpPlanner/Client/Pages/Export.razor @@ -0,0 +1,134 @@ +@page "/export" +@inject HttpClient Http +@inject KeywordCheck KeywordCheck +@inject AppState AppStateStore +@inject NavigationManager NavigationManager +@using MeetUpPlanner.Shared +@using Newtonsoft.Json +@using BlazorDownloadFile +@using System.IO + +
+

Kontaktliste

+

+ Wenn der Fall eingetreten ist, dass jemand positiv auf Covid-19 getestet wurde, braucht er/sie seine Kontakte der letzten Wochen. Dazu bitte den Export per e-mail anfordern oder sofern + das Admin-Schlüsselwort bekannt ist, hier direkt ausführen. +

+
+ + + +@code { + [Inject] public IBlazorDownloadFileService BlazorDownloadFileService { get; set; } + protected IEnumerable log = new List(); + TrackingReportRequest trackingRequest = new TrackingReportRequest(); + + protected override async Task OnInitializedAsync() + { + if (null != AppStateStore && !String.IsNullOrEmpty(AppStateStore.KeyWord)) + { + // Get list of log items + await ReadData(); + trackingRequest.RequestorFirstName = AppStateStore.FirstName; + trackingRequest.RequestorLastName = AppStateStore.LastName; + } + else + { + NavigationManager.NavigateTo("/"); + } + } + protected async Task ReadData() + { + string keyword = AppStateStore.KeyWord; + // Get log + if (KeywordCheck.IsAdmin) + { + log = await Http.GetFromJsonAsync>($"calendar/getexportlog?keyword={keyword}"); + } + } + + protected async Task StartExport() + { + // Get tracking report + string keyword = AppStateStore.KeyWord; + HttpResponseMessage response = await Http.PostAsJsonAsync($"Calendar/requesttrackingreport?keyword={keyword}", trackingRequest); + string responseBody = await response.Content.ReadAsStringAsync(); + TrackingReport report = JsonConvert.DeserializeObject(responseBody); + + // Filename for export file + string fileName = String.Format("{0:yyyy}-{1:MM}-{2:dd}{3}_{4}.csv", report.CreationDate, report.CreationDate, report.CreationDate, report.ReportRequest.TrackFirstName, report.ReportRequest.TrackLastName); + + // Assemble Excel (CSV) file + StringWriter csvData = new StringWriter(); + // Write header line + csvData.Write("Mitfahrer;"); + foreach(CompanionCalendarInfo c in report.CalendarList) + { + csvData.Write(c.StartDate.ToString("dd.MM.yyyy HH:mm") + ";"); + } + csvData.WriteLine(); + // Write line for every companion + foreach(Companion c in report.CompanionList) + { + csvData.Write(c.FirstName + " " + c.LastName + " - " + c.AddressInfo + ";"); + foreach (bool b in c.EventList) + { + csvData.Write(b ? "1;" : " ;"); + } + csvData.WriteLine(); + } + + await BlazorDownloadFileService.DownloadFileFromText(fileName, csvData.ToString(), "text/csv"); + await ReadData(); + } + +} diff --git a/MeetUpPlanner/Client/Pages/NewMeetUp.razor b/MeetUpPlanner/Client/Pages/NewMeetUp.razor index 4be0f88..4a08134 100644 --- a/MeetUpPlanner/Client/Pages/NewMeetUp.razor +++ b/MeetUpPlanner/Client/Pages/NewMeetUp.razor @@ -6,17 +6,17 @@ @using MeetUpPlanner.Shared @using Blazored.TextEditor -@if (!String.IsNullOrEmpty(AppStateStore.ClientSettings.NewMeetupMessage)) +@if (!String.IsNullOrEmpty(AppStateStore.ClientSettings?.NewMeetupMessage)) {
- @if (!String.IsNullOrEmpty(AppStateStore.ClientSettings.LogoLink)) + @if (!String.IsNullOrEmpty(AppStateStore.ClientSettings?.LogoLink)) {
- +
}
- @((MarkupString)AppStateStore.ClientSettings.NewMeetupMessage) + @((MarkupString)AppStateStore.ClientSettings?.NewMeetupMessage)
} @@ -60,8 +60,7 @@ - @if (null != meetup.Summary) - {@((MarkupString)meetup.Summary)} + @if (null != meetup.Summary){@((MarkupString)meetup.Summary)}
@@ -109,7 +108,7 @@ - Hier kann die Größe der Gruppe entsprechend beschränkt werden. Maximale Gruppengröße ist momentan @(AppStateStore.ClientSettings.MaxGroupSize). + Hier kann die Größe der Gruppe entsprechend beschränkt werden. Maximale Gruppengröße ist momentan @(AppStateStore.ClientSettings?.MaxGroupSize).
@@ -136,7 +135,7 @@ public string ItemId { get; set; } - private string logMessage; + private string logMessage = ""; CalendarItem meetup = new CalendarItem(); DateTime whenTime = DateTime.Now; DateTime whenDate = DateTime.Now; @@ -144,37 +143,43 @@ protected override async Task OnInitializedAsync() { - if (!String.IsNullOrEmpty(ItemId)) + if (null != AppStateStore && !String.IsNullOrEmpty(AppStateStore.KeyWord)) { - string keyword = AppStateStore.KeyWord; - // Get referenced calendar item - meetup = await Http.GetFromJsonAsync($"Calendar/calendaritem?keyword={keyword}&itemId={ItemId}"); + if (!String.IsNullOrEmpty(ItemId)) + { + string keyword = AppStateStore.KeyWord; + // Get referenced calendar item + meetup = await Http.GetFromJsonAsync($"Calendar/calendaritem?keyword={keyword}&itemId={ItemId}"); + } + else + { + meetup.HostFirstName = AppStateStore.FirstName; + meetup.HostLastName = AppStateStore.LastName; + meetup.HostAdressInfo = AppStateStore.PhoneMail; + meetup.MaxRegistrationsCount = AppStateStore.ClientSettings.MaxGroupSize; + } + whenDate = meetup.StartDate; + whenTime = meetup.StartDate; } else { - meetup.HostFirstName = AppStateStore.FirstName; - meetup.HostLastName = AppStateStore.LastName; - meetup.MaxRegistrationsCount = AppStateStore.ClientSettings.MaxGroupSize; + NavigationManager.NavigateTo("/"); } - whenDate = meetup.StartDate; - whenTime = meetup.StartDate; } private async Task OnSave() { - meetup.StartDate = new DateTime(whenDate.Year, whenDate.Month, whenDate.Day, whenTime.Hour, whenTime.Minute, whenTime.Second); - meetup.Summary = await htmlDescription.GetHTML(); - if (meetup.MaxRegistrationsCount > AppStateStore.ClientSettings.MaxGroupSize) { logMessage = "Max. Gruppengröße überschritten."; return; } + meetup.StartDate = new DateTime(whenDate.Year, whenDate.Month, whenDate.Day, whenTime.Hour, whenTime.Minute, whenTime.Second); + meetup.Summary = await htmlDescription.GetHTML(); string keyword = AppStateStore.KeyWord; // Save calendarItem server await Http.PostAsJsonAsync($"Calendar/writecalendaritem?keyword={keyword}", meetup); - logMessage = meetup.StartDate.ToString(); NavigationManager.NavigateTo("/calendar"); } protected void OnCancel() diff --git a/MeetUpPlanner/Client/Program.cs b/MeetUpPlanner/Client/Program.cs index 0045b5e..635b1cd 100644 --- a/MeetUpPlanner/Client/Program.cs +++ b/MeetUpPlanner/Client/Program.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using BlazorDownloadFile; using Blazored.LocalStorage; using MeetUpPlanner.Shared; @@ -23,6 +24,7 @@ public static async Task Main(string[] args) builder.Services.AddSingleton(); builder.Services.AddBlazoredLocalStorage(); builder.Services.AddTransient(); + builder.Services.AddBlazorDownloadFile(); await builder.Build().RunAsync(); } } diff --git a/MeetUpPlanner/Client/Shared/NavMenu.razor b/MeetUpPlanner/Client/Shared/NavMenu.razor index 734dafb..7914008 100644 --- a/MeetUpPlanner/Client/Shared/NavMenu.razor +++ b/MeetUpPlanner/Client/Shared/NavMenu.razor @@ -26,7 +26,12 @@ + - diff --git a/MeetUpPlanner/Server/Controllers/CalendarController.cs b/MeetUpPlanner/Server/Controllers/CalendarController.cs index cee9b1b..05a79de 100644 --- a/MeetUpPlanner/Server/Controllers/CalendarController.cs +++ b/MeetUpPlanner/Server/Controllers/CalendarController.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -92,5 +93,21 @@ public async Task DeleteCalendarItem([FromQuery] string keyword, BackendResult result = await _meetUpFunctions.DeleteCalendarItem(keyword, calendarItem); return Ok(result); } + [HttpPost("requesttrackingreport")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task ExportTrackingReport([FromQuery] string keyword, [FromBody] TrackingReportRequest request) + { + TrackingReport report = await _meetUpFunctions.ExportTrackingReport(keyword, request); + return Ok(report); + } + + [HttpGet("getexportlog")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetExportLog([FromQuery] string keyword) + { + IEnumerable result = await _meetUpFunctions.GetExportLog(keyword); + return Ok(result); + } + } } diff --git a/MeetUpPlanner/Server/Controllers/UtilController.cs b/MeetUpPlanner/Server/Controllers/UtilController.cs index 3184ffd..86e4595 100644 --- a/MeetUpPlanner/Server/Controllers/UtilController.cs +++ b/MeetUpPlanner/Server/Controllers/UtilController.cs @@ -16,7 +16,7 @@ public class UtilController : ControllerBase { private readonly MeetUpFunctions _meetUpFunctions; private readonly ILogger logger; - const string serverVersion = "2020-07-28"; + const string serverVersion = "2020-08-02"; string functionsVersion = "2020-07-03"; public UtilController(ILogger logger, MeetUpFunctions meetUpFunctions) diff --git a/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs b/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs index 4db1ff8..bb84006 100644 --- a/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs +++ b/MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs @@ -160,7 +160,23 @@ public async Task DeleteCalendarItem(string keyword, CalendarItem .ReceiveJson(); return result; } - + public async Task ExportTrackingReport(string keyword, TrackingReportRequest request) + { + TrackingReport result = await $"https://{_functionsConfig.FunctionAppName}.azurewebsites.net/api/ExportTrackingReport" + .WithHeader(HEADER_FUNCTIONS_KEY, _functionsConfig.ApiKey) + .WithHeader(HEADER_KEYWORD, keyword) + .PostJsonAsync(request) + .ReceiveJson(); + return result; + } + public async Task> GetExportLog(string keyword) + { + IEnumerable result = await $"https://{_functionsConfig.FunctionAppName}.azurewebsites.net/api/GetExportLog" + .WithHeader(HEADER_FUNCTIONS_KEY, _functionsConfig.ApiKey) + .WithHeader(HEADER_KEYWORD, keyword) + .GetJsonAsync>(); + return result; + } } diff --git a/MeetUpPlanner/Shared/CalendarItem.cs b/MeetUpPlanner/Shared/CalendarItem.cs index 6c77a9d..075a215 100644 --- a/MeetUpPlanner/Shared/CalendarItem.cs +++ b/MeetUpPlanner/Shared/CalendarItem.cs @@ -19,6 +19,9 @@ public class CalendarItem : CosmosDBEntity public string HostFirstName { get; set; } [JsonProperty(PropertyName = "hostLastName", NullValueHandling = NullValueHandling.Ignore), MaxLength(100), Required(ErrorMessage = "Gastgeber bitte eingeben.")] public string HostLastName { get; set; } + [JsonProperty(PropertyName = "hostAddressName", NullValueHandling = NullValueHandling.Ignore), MaxLength(100)] + public string HostAdressInfo { get; set; } + [JsonProperty(PropertyName = "summary", NullValueHandling = NullValueHandling.Ignore), Display(Name = "Zusammenfassung", Prompt = "Kurze Zusammenfassung des Termins"), MaxLength(5000, ErrorMessage = "Zusammenfassung zu lang.")] public string Summary { get; set; } [JsonProperty(PropertyName = "maxRegistrationsCount", NullValueHandling = NullValueHandling.Ignore), Range(2.0, 50.0, ErrorMessage = "Gruppengröße nicht im gültigen Bereich."), Display(Name = "Maximale Anzahl Teilnehmer", Prompt = "Anzahl eingeben"), Required(ErrorMessage = "Max. Anzahl Teilnehmer eingeben")] @@ -76,7 +79,7 @@ public string GetStartDateAsString() string dateString = String.Empty; if (null != StartDate) { - dateString = weekdays[(int)StartDate.DayOfWeek] + ", " + this.StartDate.ToString("dd.MM.yyy HH:mm") + " Uhr"; + dateString = weekdays[(int)StartDate.DayOfWeek] + ", " + this.StartDate.ToString("dd.MM HH:mm") + " Uhr"; } return dateString; } diff --git a/MeetUpPlanner/Shared/Companion.cs b/MeetUpPlanner/Shared/Companion.cs new file mode 100644 index 0000000..30cd6aa --- /dev/null +++ b/MeetUpPlanner/Shared/Companion.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Newtonsoft.Json; + + +namespace MeetUpPlanner.Shared +{ + /// + /// Class to get contact details of a companion during an event in the past + /// + public class Companion + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string AddressInfo { get; set; } + /// + /// List of true/false indicators showing if the user was part of the corresponding item in the list of CompanionCalendarInfo items + /// in the TrackingReport + /// + public IList EventList { get; set; } + public Companion() + { + + } + public Companion(string firstName, string lastName, string addressInfo, int eventListSize) + { + FirstName = firstName; + LastName = lastName; + AddressInfo = addressInfo; + EventList = new List(eventListSize); + for (int i = 0; i < eventListSize; ++ i) + { + EventList.Add(false); + } + } + } +} diff --git a/MeetUpPlanner/Shared/CompanionCalendarInfo.cs b/MeetUpPlanner/Shared/CompanionCalendarInfo.cs new file mode 100644 index 0000000..4d34161 --- /dev/null +++ b/MeetUpPlanner/Shared/CompanionCalendarInfo.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace MeetUpPlanner.Shared +{ + /// + /// Class holds the info if a companion was part of the event + /// + public class CompanionCalendarInfo + { + public string Title { get; set; } + public DateTime StartDate { get; set; } + public string HostFirstName { get; set; } + public string HostLastName { get; set; } + public string HostAddressInfo { get; set; } + public string LevelDescription { get; set; } + + public CompanionCalendarInfo() + { + + } + public CompanionCalendarInfo(CalendarItem calendarItem) + { + Title = calendarItem.Title; + StartDate = calendarItem.StartDate; + HostFirstName = calendarItem.HostFirstName; + HostLastName = calendarItem.HostLastName; + HostAddressInfo = calendarItem.HostAdressInfo; + LevelDescription = calendarItem.LevelDescription; + } + } +} diff --git a/MeetUpPlanner/Shared/ExportLogItem.cs b/MeetUpPlanner/Shared/ExportLogItem.cs new file mode 100644 index 0000000..66e5827 --- /dev/null +++ b/MeetUpPlanner/Shared/ExportLogItem.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MeetUpPlanner.Shared +{ + public class ExportLogItem : CosmosDBEntity + { + public string RequestorFirstName { get; set; } + public string RequestorLastName { get; set; } + public string RequestedFirstName { get; set; } + public string RequestedLastName { get; set; } + public DateTime RequestDate { get; set; } = DateTime.Now; + public string RequestReason { get; set; } + + public ExportLogItem() + { + + } + public ExportLogItem(TrackingReportRequest reportRequest) + { + RequestorFirstName = reportRequest.RequestorFirstName; + RequestorLastName = reportRequest.RequestorLastName; + RequestedFirstName = reportRequest.TrackFirstName; + RequestedLastName = reportRequest.TrackLastName; + RequestDate = DateTime.Now; + RequestReason = reportRequest.Comment; + + } + } +} diff --git a/MeetUpPlanner/Shared/ExtendedCalendarItem.cs b/MeetUpPlanner/Shared/ExtendedCalendarItem.cs index 0365461..e803082 100644 --- a/MeetUpPlanner/Shared/ExtendedCalendarItem.cs +++ b/MeetUpPlanner/Shared/ExtendedCalendarItem.cs @@ -33,6 +33,7 @@ public ExtendedCalendarItem(CalendarItem calendarItem) this.Place = calendarItem.Place; this.HostFirstName = calendarItem.HostFirstName; this.HostLastName = calendarItem.HostLastName; + this.HostAdressInfo = calendarItem.HostAdressInfo; this.Summary = calendarItem.Summary; this.MaxRegistrationsCount = calendarItem.MaxRegistrationsCount; this.PrivateKeyword = calendarItem.PrivateKeyword; diff --git a/MeetUpPlanner/Shared/Participant.cs b/MeetUpPlanner/Shared/Participant.cs index c39c547..b70aa15 100644 --- a/MeetUpPlanner/Shared/Participant.cs +++ b/MeetUpPlanner/Shared/Participant.cs @@ -18,6 +18,8 @@ public class Participant : CosmosDBEntity public string ParticipantFirstName { get; set; } [JsonProperty(PropertyName = "participantLastName"), MaxLength(100), Required(ErrorMessage = "Nachnamen bitte eingeben.")] public string ParticipantLastName { get; set; } + [JsonProperty(PropertyName = "participantAddressName", NullValueHandling = NullValueHandling.Ignore), MaxLength(100) ] + public string ParticipantAdressInfo { get; set; } [JsonProperty(PropertyName = "checkInDate")] public DateTime CheckInDate { get; set; } [JsonIgnore] diff --git a/MeetUpPlanner/Shared/TrackingReport.cs b/MeetUpPlanner/Shared/TrackingReport.cs new file mode 100644 index 0000000..4916be5 --- /dev/null +++ b/MeetUpPlanner/Shared/TrackingReport.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MeetUpPlanner.Shared +{ + /// + /// Describes a "tracking report": A list of all persons who had participated with the user in an event in the past + /// + public class TrackingReport + { + /// + /// Details about the requested report + /// + public TrackingReportRequest ReportRequest { get; set; } + public DateTime CreationDate { get; set; } = DateTime.Now; + /// + /// List of all events in scope + /// + public IEnumerable CalendarList { get; set; } + /// + /// List of all companions in scope + /// + public IList CompanionList { get; set; } + + public TrackingReport() + { + } + /// + /// Constructs a TrackingReport with the given request + /// + /// + public TrackingReport(TrackingReportRequest request) + { + ReportRequest = request; + } + } +} diff --git a/MeetUpPlanner/Shared/TrackingReportRequest.cs b/MeetUpPlanner/Shared/TrackingReportRequest.cs new file mode 100644 index 0000000..d87c93b --- /dev/null +++ b/MeetUpPlanner/Shared/TrackingReportRequest.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.ComponentModel.DataAnnotations; + + +namespace MeetUpPlanner.Shared +{ + /// + /// Request to get a tracking report + /// + public class TrackingReportRequest + { + public string RequestorFirstName { get; set; } + public string RequestorLastName { get; set; } + [Required(ErrorMessage = "Bitte Vornamen angeben.")] + public string TrackFirstName { get; set; } + [Required(ErrorMessage = "Bitte Nachnamen angeben.")] + public string TrackLastName { get; set; } + [Required(ErrorMessage = "Bitte einen Grund angeben.")] + public string Comment { get; set; } + } +}