/// <summary> /// Method that creates the Open Shift Entity Mapping, posts to Graph, and saves the data in Azure /// table storage upon successful creation in Graph. /// </summary> /// <param name="accessToken">The Graph access token.</param> /// <param name="openShiftNotFound">The open shift to post to Graph.</param> /// <param name="monthPartitionKey">The monthwise partition key.</param> /// <param name="mappedTeam">The mapped team.</param> /// <returns>A unit of execution.</returns> private async Task CreateEntryOpenShiftsEntityMappingAsync( string accessToken, List <OpenShiftRequestModel> openShiftNotFound, string monthPartitionKey, TeamToDepartmentJobMappingEntity mappedTeam) { this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync start at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); // This foreach loop iterates over the OpenShifts which are to be added into Shifts UI. foreach (var item in openShiftNotFound) { this.telemetryClient.TrackTrace($"Processing the open shift entity with schedulingGroupId: {item.SchedulingGroupId}"); // create entries from not found list var telemetryProps = new Dictionary <string, string>() { { "SchedulingGroupId", item.SchedulingGroupId }, }; var requestString = JsonConvert.SerializeObject(item); var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "teams/" + mappedTeam.TeamId + "/schedule/openShifts") { Content = new StringContent(requestString, Encoding.UTF8, "application/json"), }) { var response = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var openShiftResponse = JsonConvert.DeserializeObject <Models.Response.OpenShifts.GraphOpenShift>(responseContent); var openShiftMappingEntity = this.CreateNewOpenShiftMappingEntity(openShiftResponse, item.KronosUniqueId, monthPartitionKey, mappedTeam?.RowKey); telemetryProps.Add("ResultCode", response.StatusCode.ToString()); telemetryProps.Add("TeamsOpenShiftId", openShiftResponse.Id); this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, telemetryProps); await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(openShiftMappingEntity).ConfigureAwait(false); } else { var errorProps = new Dictionary <string, string>() { { "ResultCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "SchedulingGroupId", item.SchedulingGroupId }, { "MappedTeamId", mappedTeam?.TeamId }, }; // Have the log to capture the 403. this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, errorProps); } } } this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync end at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); }
/// <summary> /// Method that will delete an orphaned open shift entity. /// </summary> /// <param name="allRequiredConfiguration">The required configuration details.</param> /// <param name="lookUpDataFoundList">The found open shifts.</param> /// <param name="lookUpData">All of the look up (reference data).</param> /// <param name="mappedTeam">The list of mapped teams.</param> /// <returns>A unit of execution.</returns> private async Task DeleteOrphanDataOpenShiftsEntityMappingAsync( SetupDetails allRequiredConfiguration, List <AllOpenShiftMappingEntity> lookUpDataFoundList, List <AllOpenShiftMappingEntity> lookUpData, TeamToDepartmentJobMappingEntity mappedTeam) { // delete entries from orphan list var orphanList = lookUpData.Except(lookUpDataFoundList); this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync started at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); // This foreach loop iterates over items that are to be deleted from Shifts UI. In other // words, these are Open Shifts which have been deleted in Kronos WFC, and those deletions // are propagating to Shifts. foreach (var item in orphanList) { this.telemetryClient.TrackTrace($"OpenShiftController - Checking {item.RowKey} to see if there are any Open Shift Requests"); var isInOpenShiftRequestMappingTable = await this.openShiftRequestMappingEntityProvider.CheckOpenShiftRequestExistance(item.RowKey).ConfigureAwait(false); if (!isInOpenShiftRequestMappingTable) { this.telemetryClient.TrackTrace($"{item.RowKey} is not in the Open Shift Request mapping table - deletion can be done."); await this.DeleteOpenShiftInTeams(allRequiredConfiguration, item, mappedTeam).ConfigureAwait(false); } else { // Log that the open shift exists in another table and it should not be deleted. this.telemetryClient.TrackTrace($"OpenShiftController - Open Shift ID: {item.RowKey} is being handled by another process."); } } this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync ended at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); }
/// <summary> /// This method will remove open shifts from Teams in the event we find more OS in cache /// than retrieved from Kronos. /// </summary> /// <param name="allRequiredConfigurations">The required configuration details.</param> /// <param name="mappedOrgJobEntity">The team deatils.</param> /// <param name="openShiftsToProcess">All of the matching entities in cache.</param> /// <param name="numberOfOpenShiftsToRemove">The number of matching entities we want to remove.</param> /// <returns>A unit of execution.</returns> private async Task RemoveAdditionalOpenShiftsFromTeamsAsync( SetupDetails allRequiredConfigurations, TeamToDepartmentJobMappingEntity mappedOrgJobEntity, List <AllOpenShiftMappingEntity> openShiftsToProcess, int numberOfOpenShiftsToRemove) { var totalSlotsInCache = 0; openShiftsToProcess.ForEach(x => totalSlotsInCache += x.KronosSlots); if (!openShiftsToProcess.Any() || totalSlotsInCache < numberOfOpenShiftsToRemove) { // This code should not ever be used in theory however it protects against an infinite loop. this.telemetryClient.TrackTrace($"Error when removing open shifts from Teams. We need to remove {numberOfOpenShiftsToRemove} slots from cache but there is only {totalSlotsInCache} remaining in cache."); return; } do { // Find the mapping entity with the most slots var mappingEntityToDecrement = openShiftsToProcess.OrderByDescending(x => x.KronosSlots).First(); if (mappingEntityToDecrement.KronosSlots - numberOfOpenShiftsToRemove > 0) { // More slots than what we need to remove so update slot count in Teams and update cache. var teamsOpenShiftEntity = await this.GetOpenShiftFromTeams(allRequiredConfigurations, mappedOrgJobEntity, mappingEntityToDecrement).ConfigureAwait(false); if (teamsOpenShiftEntity != null) { teamsOpenShiftEntity.SharedOpenShift.OpenSlotCount -= numberOfOpenShiftsToRemove; var response = await this.UpdateOpenShiftInTeams(allRequiredConfigurations, teamsOpenShiftEntity, mappedOrgJobEntity).ConfigureAwait(false); if (response.IsSuccessStatusCode) { mappingEntityToDecrement.KronosSlots -= numberOfOpenShiftsToRemove; await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(mappingEntityToDecrement).ConfigureAwait(false); numberOfOpenShiftsToRemove = 0; } } } else { // We need to remove more so delete the entity in Teams and delete from cache var response = await this.DeleteOpenShiftInTeams(allRequiredConfigurations, mappingEntityToDecrement, mappedOrgJobEntity).ConfigureAwait(false); if (response.IsSuccessStatusCode) { numberOfOpenShiftsToRemove -= mappingEntityToDecrement.KronosSlots; } } // Remove the mapping entity so we can process the next entity if necessary. openShiftsToProcess.Remove(mappingEntityToDecrement); }while (numberOfOpenShiftsToRemove > 0); }
/// <summary> /// This method will cause the thread to wait allowing us to retrun a success response /// for the delete WFI request. It will then share the changes. /// </summary> /// <param name="shift">The shift we want to share.</param> /// <param name="mappedTeam">The team details of the schedule we want to share.</param> /// <param name="allRequiredConfigurations">The required configuration.</param> /// <returns>A unit of execution.</returns> private async Task ShareScheduleAfterShiftDeletion(ShiftsShift shift, TeamToDepartmentJobMappingEntity mappedTeam, IntegrationApi.SetupDetails allRequiredConfigurations) { // We want to wait so that there is time to respond a success to the WFI request // meaning the shift will be deleted in Teams. Thread.Sleep(int.Parse(appSettings.AutoShareScheduleWaitTime)); // We now want to share the schedule between the start and end time of the deleted shift. await this.graphUtility.ShareSchedule( allRequiredConfigurations.GraphConfigurationDetails, mappedTeam.TeamId, shift.SharedShift.StartDateTime, shift.SharedShift.EndDateTime, false).ConfigureAwait(false); }
/// <summary> /// Method to save or update Teams to Department mapping. /// </summary> /// <param name="entity">Mapping entity reference.</param> /// <returns>http status code representing the asynchronous operation.</returns> public async Task <bool> SaveOrUpdateTeamsToDepartmentMappingAsync(TeamToDepartmentJobMappingEntity entity) { if (entity is null) { throw new ArgumentNullException(nameof(entity)); } try { var result = await this.StoreOrUpdateEntityAsync(entity).ConfigureAwait(false); return(result.HttpStatusCode == (int)HttpStatusCode.NoContent); } catch (Exception) { return(false); throw; } }
/// <summary> /// Creates and stores a shift mapping entity. /// </summary> /// <param name="shift">A shift from Shifts.</param> /// <param name="user">A user mapping entity.</param> /// <param name="mappedTeam">A team mapping entity.</param> /// <param name="monthPartitionKey">The partition key for the shift.</param> /// <returns>A task.</returns> private async Task CreateAndStoreShiftMapping(ShiftsShift shift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam, List <string> monthPartitionKey) { var kronosUniqueId = this.utility.CreateShiftUniqueId(shift, mappedTeam.KronosTimeZone); var shiftMappingEntity = this.CreateNewShiftMappingEntity(shift, kronosUniqueId, user.RowKey, mappedTeam.TeamId); await this.shiftMappingEntityProvider.SaveOrUpdateShiftMappingEntityAsync(shiftMappingEntity, shift.Id, monthPartitionKey[0]).ConfigureAwait(false); }
/// <summary> /// Edits a shift in Kronos and updates the database. /// </summary> /// <param name="editedShift">The shift to edit.</param> /// <param name="user">The user the shift is for.</param> /// <param name="mappedTeam">The team the user is in.</param> /// <returns>A response for teams.</returns> public async Task <ShiftsIntegResponse> EditShiftInKronosAsync(ShiftsShift editedShift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam) { // The connector does not support drafting entities as it is not possible to draft shifts in Kronos. // Likewise there is no share schedule WFI call. if (editedShift.DraftShift != null) { return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "Editing a shift as a draft is not supported for your team in Teams. Please publish changes directly using the 'Share' button.")); } if (editedShift.SharedShift == null) { return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "An unexpected error occured. Could not edit the shift.")); } // We use the display name to indicate shift transfers. As we cannot support editing shifts // with a transfer we block edits on shifts containing the transfer string. if (editedShift.SharedShift.DisplayName.Contains(appSettings.TransferredShiftDisplayName)) { return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "You can't edit a shift that includes a shift transfer. Please make your changes in Kronos")); } // We do not support editing activities in Teamsand cannot support editing shift transfers // therefore we only expect activities with the regular segment type. Anything else means the // manager has modified or added an activity. var invalidActivities = editedShift.SharedShift.Activities.Where(x => x.DisplayName != ApiConstants.RegularSegmentType); if (invalidActivities.Any()) { return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "Editing shift activities is not supported for your team in Teams.")); } var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false); if ((allRequiredConfigurations?.IsAllSetUpExists).ErrorIfNull(editedShift.Id, "App configuration incorrect.", out var response)) { return(response); } // We need to get all other shifts the employee works that day. var kronosStartDateTime = this.utility.UTCToKronosTimeZone(editedShift.SharedShift.StartDateTime, mappedTeam.KronosTimeZone); var kronosEndDateTime = this.utility.UTCToKronosTimeZone(editedShift.SharedShift.EndDateTime, mappedTeam.KronosTimeZone); var monthPartitionKey = Utility.GetMonthPartition(this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime)); var shiftToReplace = await this.shiftMappingEntityProvider.GetShiftMappingEntityByRowKeyAsync(editedShift.Id).ConfigureAwait(false); var shiftToReplaceStartDateTime = this.utility.UTCToKronosTimeZone(shiftToReplace.ShiftStartDate, mappedTeam.KronosTimeZone); var shiftToReplaceEndDateTime = this.utility.UTCToKronosTimeZone(shiftToReplace.ShiftEndDate, mappedTeam.KronosTimeZone); var commentTimeStamp = this.utility.UTCToKronosTimeZone(DateTime.UtcNow, mappedTeam.KronosTimeZone).ToString(CultureInfo.InvariantCulture); var shiftComments = XmlHelper.GenerateEditedShiftKronosComments(editedShift.SharedShift.Notes, this.appSettings.ShiftNotesCommentText, commentTimeStamp); var editResponse = await this.shiftsActivity.EditShift( new Uri(allRequiredConfigurations.WfmEndPoint), allRequiredConfigurations.KronosSession, this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime), kronosEndDateTime.Day > kronosStartDateTime.Day, Utility.OrgJobPathKronosConversion(user.PartitionKey), user.RowKey, kronosStartDateTime.TimeOfDay.ToString(), kronosEndDateTime.TimeOfDay.ToString(), this.utility.FormatDateForKronos(shiftToReplaceStartDateTime), this.utility.FormatDateForKronos(shiftToReplaceEndDateTime), shiftToReplaceStartDateTime.TimeOfDay.ToString(), shiftToReplaceEndDateTime.TimeOfDay.ToString(), shiftComments).ConfigureAwait(false); if (editResponse.Status != Success) { return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "Shift could not be edited in Kronos.")); } await this.DeleteShiftMapping(editedShift).ConfigureAwait(false); await this.CreateAndStoreShiftMapping(editedShift, user, mappedTeam, monthPartitionKey).ConfigureAwait(false); return(ResponseHelper.CreateSuccessfulResponse(editedShift.Id)); }
/// <summary> /// Adds the shift to Kronos and the database. /// </summary> /// <param name="shift">The shift to add.</param> /// <param name="user">The user the shift is for.</param> /// <param name="mappedTeam">The team the user is in.</param> /// <returns>A response for teams.</returns> public async Task <ShiftsIntegResponse> CreateShiftInKronosAsync(ShiftsShift shift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam) { // The connector does not support drafting entities as it is not possible to draft shifts in Kronos. // Likewise there is no share schedule WFI call. if (shift.DraftShift != null) { return(ResponseHelper.CreateBadResponse(shift.Id, error: "Creating a shift as a draft is not supported for your team in Teams. Please publish changes directly using the 'Share' button.")); } if (shift.SharedShift == null) { return(ResponseHelper.CreateBadResponse(shift.Id, error: "An unexpected error occured. Could not create the shift.")); } if (shift.SharedShift.Activities.Any()) { return(ResponseHelper.CreateBadResponse(shift.Id, error: "Adding activities to shifts is not supported for your team in Teams. Remove all activities and try sharing again.")); } if (user.ErrorIfNull(shift.Id, "User could not be found.", out var response)) { return(response); } var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false); if ((allRequiredConfigurations?.IsAllSetUpExists).ErrorIfNull(shift.Id, "App configuration incorrect.", out response)) { return(response); } var kronosStartDateTime = this.utility.UTCToKronosTimeZone(shift.SharedShift.StartDateTime, mappedTeam.KronosTimeZone); var kronosEndDateTime = this.utility.UTCToKronosTimeZone(shift.SharedShift.EndDateTime, mappedTeam.KronosTimeZone); var commentTimeStamp = this.utility.UTCToKronosTimeZone(DateTime.UtcNow, mappedTeam.KronosTimeZone).ToString(CultureInfo.InvariantCulture); var comments = XmlHelper.GenerateKronosComments(shift.SharedShift.Notes, this.appSettings.ShiftNotesCommentText, commentTimeStamp); var creationResponse = await this.shiftsActivity.CreateShift( new Uri(allRequiredConfigurations.WfmEndPoint), allRequiredConfigurations.KronosSession, this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime), kronosEndDateTime.Day > kronosStartDateTime.Day, Utility.OrgJobPathKronosConversion(user.PartitionKey), user.RowKey, kronosStartDateTime.TimeOfDay.ToString(), kronosEndDateTime.TimeOfDay.ToString(), comments).ConfigureAwait(false); if (creationResponse.Status != Success) { return(ResponseHelper.CreateBadResponse(shift.Id, error: "Shift was not created successfully in Kronos.")); } var monthPartitionKey = Utility.GetMonthPartition(this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime)); await this.CreateAndStoreShiftMapping(shift, user, mappedTeam, monthPartitionKey).ConfigureAwait(false); return(ResponseHelper.CreateSuccessfulResponse(shift.Id)); }
/// <summary> /// Deletes the shift from Kronos and the database. /// </summary> /// <param name="shift">The shift to remove.</param> /// <param name="user">The user the shift is for.</param> /// <param name="mappedTeam">The team the user is in.</param> /// <returns>A response for teams.</returns> public async Task <ShiftsIntegResponse> DeleteShiftInKronosAsync(ShiftsShift shift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam) { if (shift.SharedShift == null) { return(ResponseHelper.CreateBadResponse(shift.Id, error: "An unexpected error occured. Could not delete the shift.")); } if (user.ErrorIfNull(shift.Id, "User could not be found.", out var response)) { return(response); } var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false); if ((allRequiredConfigurations?.IsAllSetUpExists == false).ErrorIfNull(shift.Id, "App configuration incorrect.", out response)) { return(response); } // Convert to Kronos local time. var kronosStartDateTime = this.utility.UTCToKronosTimeZone(shift.SharedShift.StartDateTime, mappedTeam.KronosTimeZone); var kronosEndDateTime = this.utility.UTCToKronosTimeZone(shift.SharedShift.EndDateTime, mappedTeam.KronosTimeZone); var deletionResponse = await this.shiftsActivity.DeleteShift( new Uri(allRequiredConfigurations.WfmEndPoint), allRequiredConfigurations.KronosSession, this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime), kronosEndDateTime.Day > kronosStartDateTime.Day, Utility.OrgJobPathKronosConversion(user.PartitionKey), user.RowKey, kronosStartDateTime.TimeOfDay.ToString(), kronosEndDateTime.TimeOfDay.ToString()).ConfigureAwait(false); if (deletionResponse.Status != Success) { return(ResponseHelper.CreateBadResponse(shift.Id, error: "Shift was not successfully removed from Kronos.")); } await this.DeleteShiftMapping(shift).ConfigureAwait(false); #pragma warning disable CS4014 // We do not want to await this call as we need the shift to be deleted in Teams before sharing the schedule. Task.Run(() => this.ShareScheduleAfterShiftDeletion(shift, mappedTeam, allRequiredConfigurations)); #pragma warning restore CS4014 return(ResponseHelper.CreateSuccessfulResponse(shift.Id)); }
/// <summary> /// Method that will delete an orphaned open shift entity. /// </summary> /// <param name="accessToken">The MS Graph Access token.</param> /// <param name="lookUpDataFoundList">The found open shifts.</param> /// <param name="lookUpData">All of the look up (reference data).</param> /// <param name="mappedTeam">The list of mapped teams.</param> /// <returns>A unit of execution.</returns> private async Task DeleteOrphanDataOpenShiftsEntityMappingAsync( string accessToken, List <AllOpenShiftMappingEntity> lookUpDataFoundList, List <AllOpenShiftMappingEntity> lookUpData, TeamToDepartmentJobMappingEntity mappedTeam) { // delete entries from orphan list var orphanList = lookUpData.Except(lookUpDataFoundList); this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync started at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); // This foreach loop iterates over items that are to be deleted from Shifts UI. In other // words, these are Open Shifts which have been deleted in Kronos WFC, and those deletions // are propagating to Shifts. foreach (var item in orphanList) { this.telemetryClient.TrackTrace($"OpenShiftController - Checking {item.TeamsOpenShiftId} to see if there are any Open Shift Requests"); var isInOpenShiftRequestMappingTable = await this.openShiftRequestMappingEntityProvider.CheckOpenShiftRequestExistance(item.TeamsOpenShiftId).ConfigureAwait(false); if (!isInOpenShiftRequestMappingTable) { this.telemetryClient.TrackTrace($"{item.TeamsOpenShiftId} is not in the Open Shift Request mapping table - deletion can be done."); var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Delete, "teams/" + mappedTeam.TeamId + "/schedule/openShifts/" + item.TeamsOpenShiftId)) { var response = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var successfulDeleteProps = new Dictionary <string, string>() { { "ResponseCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "MappedTeamId", mappedTeam?.TeamId }, { "OpenShiftIdToDelete", item.TeamsOpenShiftId }, }; this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, successfulDeleteProps); await this.openShiftMappingEntityProvider.DeleteOrphanDataFromOpenShiftMappingAsync(item).ConfigureAwait(false); } else { var errorDeleteProps = new Dictionary <string, string>() { { "ResponseCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "MappedTeamId", mappedTeam?.TeamId }, { "OpenShiftIdToDelete", item.TeamsOpenShiftId }, }; this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, errorDeleteProps); } } } else { // Log that the open shift exists in another table and it should not be deleted. this.telemetryClient.TrackTrace($"OpenShiftController - Open Shift ID: {item.TeamsOpenShiftId} is being handled by another process."); } } this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync ended at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); }
/// <summary> /// Delete an open shift entity from Teams. /// </summary> /// <param name="allRequiredConfiguration">The required configuration details.</param> /// <param name="openShiftMapping">The open shift entity to delete.</param> /// <param name="mappedTeam">The team details.</param> /// <returns>A unit of execution.</returns> private async Task <HttpResponseMessage> DeleteOpenShiftInTeams(SetupDetails allRequiredConfiguration, AllOpenShiftMappingEntity openShiftMapping, TeamToDepartmentJobMappingEntity mappedTeam) { var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI"); httpClient.DefaultRequestHeaders.Add("X-MS-WFMPassthrough", allRequiredConfiguration.WFIId); var requestUrl = $"teams/{mappedTeam.TeamId}/schedule/openShifts/{openShiftMapping.RowKey}"; var response = await this.graphUtility.SendHttpRequest(allRequiredConfiguration.GraphConfigurationDetails, httpClient, HttpMethod.Delete, requestUrl).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var successfulDeleteProps = new Dictionary <string, string>() { { "ResponseCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "MappedTeamId", mappedTeam?.TeamId }, { "OpenShiftIdToDelete", openShiftMapping.RowKey }, }; this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, successfulDeleteProps); await this.openShiftMappingEntityProvider.DeleteOrphanDataFromOpenShiftMappingAsync(openShiftMapping).ConfigureAwait(false); } else { var errorDeleteProps = new Dictionary <string, string>() { { "ResponseCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "MappedTeamId", mappedTeam?.TeamId }, { "OpenShiftIdToDelete", openShiftMapping.RowKey }, }; this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, errorDeleteProps); } return(response); }
/// <summary> /// Update an open shift entity in Teams. /// </summary> /// <param name="allRequiredConfiguration">The required configuration details.</param> /// <param name="openShift">The open shift entity to update.</param> /// <param name="mappedTeam">The team details.</param> /// <returns>A unit of execution.</returns> private async Task <HttpResponseMessage> UpdateOpenShiftInTeams(SetupDetails allRequiredConfiguration, GraphOpenShift openShift, TeamToDepartmentJobMappingEntity mappedTeam) { var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI"); httpClient.DefaultRequestHeaders.Add("X-MS-WFMPassthrough", allRequiredConfiguration.WFIId); var requestString = JsonConvert.SerializeObject(openShift); var requestUrl = $"teams/{mappedTeam.TeamId}/schedule/openShifts/{openShift.Id}"; var response = await this.graphUtility.SendHttpRequest(allRequiredConfiguration.GraphConfigurationDetails, httpClient, HttpMethod.Put, requestUrl, requestString).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var successfulUpdateProps = new Dictionary <string, string>() { { "ResponseCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "MappedTeamId", mappedTeam?.TeamId }, { "OpenShiftIdToDelete", openShift.Id }, }; this.telemetryClient.TrackTrace("Open shift updated.", successfulUpdateProps); } else { var errorUpdateProps = new Dictionary <string, string>() { { "ResponseCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "MappedTeamId", mappedTeam?.TeamId }, { "OpenShiftIdToDelete", openShift.Id }, }; this.telemetryClient.TrackTrace("Open shift could not be updated.", errorUpdateProps); } return(response); }
/// <summary> /// Method that creates the Open Shift Entity Mapping, posts to Graph, and saves the data in Azure /// table storage upon successful creation in Graph. /// </summary> /// <param name="allRequiredConfiguration">The required configuration details.</param> /// <param name="openShiftNotFound">The open shift to post to Graph.</param> /// <param name="monthPartitionKey">The monthwise partition key.</param> /// <param name="mappedTeam">The mapped team.</param> /// <returns>A unit of execution.</returns> private async Task CreateEntryOpenShiftsEntityMappingAsync( SetupDetails allRequiredConfiguration, List <OpenShiftRequestModel> openShiftNotFound, List <AllOpenShiftMappingEntity> lookUpEntriesFoundList, string monthPartitionKey, TeamToDepartmentJobMappingEntity mappedTeam) { this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync start at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); // This foreach loop iterates over the OpenShifts which are to be added into Shifts UI. foreach (var item in openShiftNotFound) { this.telemetryClient.TrackTrace($"Processing the open shift entity with schedulingGroupId: {item.SchedulingGroupId}"); // create entries from not found list var telemetryProps = new Dictionary <string, string>() { { "SchedulingGroupId", item.SchedulingGroupId }, }; var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI"); httpClient.DefaultRequestHeaders.Add("X-MS-WFMPassthrough", allRequiredConfiguration.WFIId); var requestString = JsonConvert.SerializeObject(item); var requestUrl = $"teams/{mappedTeam.TeamId}/schedule/openShifts"; var response = await this.graphUtility.SendHttpRequest(allRequiredConfiguration.GraphConfigurationDetails, httpClient, HttpMethod.Post, requestUrl, requestString).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var openShiftResponse = JsonConvert.DeserializeObject <Models.Response.OpenShifts.GraphOpenShift>(responseContent); var openShiftMappingEntity = this.CreateNewOpenShiftMappingEntity(openShiftResponse, item.KronosUniqueId, monthPartitionKey, mappedTeam?.RowKey); telemetryProps.Add("ResultCode", response.StatusCode.ToString()); telemetryProps.Add("TeamsOpenShiftId", openShiftResponse.Id); this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, telemetryProps); await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(openShiftMappingEntity).ConfigureAwait(false); // Add the entity to the found list to prevent later processes from deleting // the newly added entity. lookUpEntriesFoundList.Add(openShiftMappingEntity); } else { var errorProps = new Dictionary <string, string>() { { "ResultCode", response.StatusCode.ToString() }, { "ResponseHeader", response.Headers.ToString() }, { "SchedulingGroupId", item.SchedulingGroupId }, { "MappedTeamId", mappedTeam?.TeamId }, }; // Have the log to capture the error. this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, errorProps); } } this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync end at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}"); }
/// <summary> /// Retrieve an open shift from Teams by Team open shift Id. /// </summary> /// <param name="allRequiredConfigurations">The required configuration details.</param> /// <param name="mappedOrgJobEntity">The team details.</param> /// <param name="mappingEntityToDecrement">The mapping entity conatianing the details of the OS we want to retrieve.</param> /// <returns>A Graph open shift object.</returns> private async Task <GraphOpenShift> GetOpenShiftFromTeams(SetupDetails allRequiredConfigurations, TeamToDepartmentJobMappingEntity mappedOrgJobEntity, AllOpenShiftMappingEntity mappingEntityToDecrement) { var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI"); var requestUrl = $"teams/{mappedOrgJobEntity.TeamId}/schedule/openShifts/{mappingEntityToDecrement.RowKey}"; var response = await this.graphUtility.SendHttpRequest(allRequiredConfigurations.GraphConfigurationDetails, httpClient, HttpMethod.Get, requestUrl).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return(JsonConvert.DeserializeObject <GraphOpenShift>(responseContent)); } else { this.telemetryClient.TrackTrace($"The open shift with id {mappingEntityToDecrement.RowKey} could not be found in Teams. "); return(null); } }
/// <summary> /// Generate a Teams open shift entity. /// </summary> /// <param name="kronosOpenShift">the Kronos open shift object.</param> /// <param name="mappedOrgJobEntity">The team details.</param> /// <returns>An open shift request model.</returns> private OpenShiftRequestModel GenerateTeamsOpenShiftEntity(OpenShiftBatch.ScheduleShift kronosOpenShift, TeamToDepartmentJobMappingEntity mappedOrgJobEntity) { var openShiftActivity = new List <Activity>(); // This foreach loop will build the OpenShift activities. foreach (var segment in kronosOpenShift.ShiftSegments.ShiftSegment) { openShiftActivity.Add(new Activity { IsPaid = true, StartDateTime = this.utility.CalculateStartDateTime(segment, mappedOrgJobEntity.KronosTimeZone), EndDateTime = this.utility.CalculateEndDateTime(segment, mappedOrgJobEntity.KronosTimeZone), Code = string.Empty, DisplayName = segment.SegmentTypeName, }); } var openShift = new OpenShiftRequestModel() { SchedulingGroupId = mappedOrgJobEntity.TeamsScheduleGroupId, SharedOpenShift = new OpenShiftItem { DisplayName = string.Empty, OpenSlotCount = Constants.ShiftsOpenSlotCount, Notes = this.utility.GetOpenShiftNotes(kronosOpenShift), StartDateTime = openShiftActivity.First().StartDateTime, EndDateTime = openShiftActivity.Last().EndDateTime, Theme = this.appSettings.OpenShiftTheme, Activities = openShiftActivity, }, }; // Generates the uniqueId for the OpenShift. openShift.KronosUniqueId = this.utility.CreateUniqueId(openShift, mappedOrgJobEntity); return(openShift); }
/// <summary> /// Creates and stores a open shift mapping entity. /// </summary> /// <param name="openShift">An open shift from Shifts.</param> /// <param name="mappedTeam">A team mapping entity.</param> /// <param name="monthPartitionKey">The partition key for the shift.</param> /// <param name="openShiftOrgJobPath">The org job path of the open shift.</param> /// <returns>A task.</returns> private async Task CreateAndStoreOpenShiftMapping(Models.IntegrationAPI.OpenShiftIS openShift, TeamToDepartmentJobMappingEntity mappedTeam, string monthPartitionKey, string openShiftOrgJobPath) { var kronosUniqueId = this.utility.CreateOpenShiftInTeamsUniqueId(openShift, mappedTeam.KronosTimeZone, openShiftOrgJobPath); var startDateTime = DateTime.SpecifyKind(openShift.SharedOpenShift.StartDateTime, DateTimeKind.Utc); AllOpenShiftMappingEntity openShiftMappingEntity = new AllOpenShiftMappingEntity { PartitionKey = monthPartitionKey, RowKey = openShift.Id, KronosOpenShiftUniqueId = kronosUniqueId, KronosSlots = openShift.SharedOpenShift.OpenSlotCount, OrgJobPath = openShiftOrgJobPath, OpenShiftStartDate = startDateTime, }; await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(openShiftMappingEntity).ConfigureAwait(false); }
/// <summary> /// Creates an open shift in Kronos. /// </summary> /// <param name="openShift">The open shift entity to create in Kronos.</param> /// <param name="team">The team the open shift belongs to.</param> /// <returns>A response to return to teams.</returns> public async Task <ShiftsIntegResponse> CreateOpenShiftInKronosAsync(Models.IntegrationAPI.OpenShiftIS openShift, TeamToDepartmentJobMappingEntity team) { // The connector does not support drafting entities as it is not possible to draft shifts in Kronos. // Likewise there is no share schedule WFI call. if (openShift.DraftOpenShift != null) { return(ResponseHelper.CreateBadResponse(openShift.Id, error: "Creating an open shift as a draft is not supported for your team in Teams. Please publish changes directly using the 'Share' button.")); } if (openShift.SharedOpenShift == null) { return(ResponseHelper.CreateBadResponse(openShift.Id, error: "An unexpected error occured. Could not create open shift.")); } if (openShift.SharedOpenShift.Activities.Any()) { return(ResponseHelper.CreateBadResponse(openShift.Id, error: "Adding activities to open shifts is not supported for your team in Teams. Remove all activities and try sharing again.")); } var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false); if ((allRequiredConfigurations?.IsAllSetUpExists).ErrorIfNull(openShift.Id, "App configuration incorrect.", out var response)) { return(response); } var possibleTeams = await this.teamDepartmentMappingProvider.GetMappedTeamDetailsBySchedulingGroupAsync(team.TeamId, openShift.SchedulingGroupId).ConfigureAwait(false); var openShiftOrgJobPath = possibleTeams.FirstOrDefault().RowKey; var commentTimeStamp = this.utility.UTCToKronosTimeZone(DateTime.UtcNow, team.KronosTimeZone).ToString(CultureInfo.InvariantCulture); var comments = XmlHelper.GenerateKronosComments(openShift.SharedOpenShift.Notes, this.appSettings.ShiftNotesCommentText, commentTimeStamp); var openShiftDetails = new { KronosStartDateTime = this.utility.UTCToKronosTimeZone(openShift.SharedOpenShift.StartDateTime, team.KronosTimeZone), KronosEndDateTime = this.utility.UTCToKronosTimeZone(openShift.SharedOpenShift.EndDateTime, team.KronosTimeZone), DisplayName = openShift.SharedOpenShift.DisplayName, }; var creationResponse = await this.openShiftActivity.CreateOpenShiftAsync( new Uri(allRequiredConfigurations.WfmEndPoint), allRequiredConfigurations.KronosSession, this.utility.FormatDateForKronos(openShiftDetails.KronosStartDateTime), this.utility.FormatDateForKronos(openShiftDetails.KronosEndDateTime), openShiftDetails.KronosEndDateTime.Day > openShiftDetails.KronosStartDateTime.Day, Utility.OrgJobPathKronosConversion(openShiftOrgJobPath), openShiftDetails.DisplayName, openShiftDetails.KronosStartDateTime.TimeOfDay.ToString(), openShiftDetails.KronosEndDateTime.TimeOfDay.ToString(), openShift.SharedOpenShift.OpenSlotCount, comments).ConfigureAwait(false); if (creationResponse.Status != ApiConstants.Success) { return(ResponseHelper.CreateBadResponse(openShift.Id, error: "Open shift was not created successfully in Kronos.")); } var monthPartitionKey = Utility.GetMonthPartition( this.utility.FormatDateForKronos(openShiftDetails.KronosStartDateTime), this.utility.FormatDateForKronos(openShiftDetails.KronosEndDateTime)); await this.CreateAndStoreOpenShiftMapping(openShift, team, monthPartitionKey.FirstOrDefault(), openShiftOrgJobPath).ConfigureAwait(false); return(ResponseHelper.CreateSuccessfulResponse(openShift.Id)); }
public async Task <IActionResult> ImportMappingAsync() { var configurationEntities = await this.configurationProvider.GetConfigurationsAsync().ConfigureAwait(false); var configEntity = configurationEntities?.FirstOrDefault(); if (configEntity != null && !string.IsNullOrEmpty(configEntity.WorkforceIntegrationId)) { // Getting the posted file. var file = this.HttpContext.Request.Form.Files[0]; bool isValidFile = true; int noOfColumns = 0; if (file != null) { using (XLWorkbook workbook = new XLWorkbook(file.OpenReadStream())) { IXLWorksheet worksheet = workbook.Worksheet(1); // Validation to check if row other than column exists if (worksheet.RowsUsed().Count() == 1) { isValidFile = false; return(this.Json(new { isWorkforceIntegrationPresent = true, response = isValidFile })); } // Getting count of total used cells var usedCellsCount = worksheet.RowsUsed().CellsUsed().Count(); foreach (IXLRow row in worksheet.RowsUsed()) { if (row.RangeAddress.FirstAddress.RowNumber == 1) { // Getting count of total coumns available in the template noOfColumns = row.CellsUsed().Count(); continue; } // Validation to check if any cell has empty value if ((usedCellsCount % noOfColumns) != 0 || noOfColumns != Convert.ToInt16(Resources.NoOfColumnsInTeamsExcel, CultureInfo.InvariantCulture)) { isValidFile = false; return(this.Json(new { isWorkforceIntegrationPresent = true, response = isValidFile })); } TeamToDepartmentJobMappingEntity entity = new TeamToDepartmentJobMappingEntity() { PartitionKey = row.Cell(1).Value.ToString(), RowKey = Utility.OrgJobPathDBConversion(row.Cell(2).Value.ToString()), KronosTimeZone = row.Cell(3).Value.ToString(), TeamId = row.Cell(4).Value.ToString(), ShiftsTeamName = row.Cell(5).Value.ToString(), TeamsScheduleGroupId = row.Cell(6).Value.ToString(), TeamsScheduleGroupName = row.Cell(7).Value.ToString(), }; if (isValidFile) { var tenantId = this.appSettings.TenantId; var clientId = this.appSettings.ClientId; var clientSecret = this.appSettings.ClientSecret; var instance = this.appSettings.Instance; var accessToken = await this.graphUtility.GetAccessTokenAsync(tenantId, instance, clientId, clientSecret, configEntity.AdminAadObjectId).ConfigureAwait(false); var graphClient = CreateGraphClientWithDelegatedAccess(accessToken); var isSuccess = await this.graphUtility.AddWFInScheduleAsync(entity.TeamId, graphClient, configEntity.WorkforceIntegrationId, accessToken).ConfigureAwait(false); if (isSuccess) { await this.teamDepartmentMappingProvider.SaveOrUpdateTeamsToDepartmentMappingAsync(entity).ConfigureAwait(false); } } } } } return(this.Json(new { isWorkforceIntegrationPresent = true, response = isValidFile })); } else { return(this.Json(new { isWorkforceIntegrationPresent = false, error = Resources.WorkforceIntegrationNotRegister })); } }
/// <summary> /// This method finds any idnetical open shifts before performing logic specific to OS /// that occur more than once enabling open shift slot count support. /// </summary> /// <param name="allRequiredConfigurations">The required configuration details.</param> /// <param name="monthPartitionKey">The month partition key currently being synced.</param> /// <param name="openShiftsFoundList">The list of Teams open shift entities we found in cache.</param> /// <param name="lookUpEntriesFoundList">The list of mapping entities retrieved from Kronos and found in cache.</param> /// <param name="mappedOrgJobEntity">The team deatils.</param> /// <param name="lookUpData">All of the cache records retrieved for the query date span.</param> /// <returns>A unit of execution.</returns> private async Task ProcessIdenticalOpenShifts( SetupDetails allRequiredConfigurations, string monthPartitionKey, List <OpenShiftRequestModel> openShiftsFoundList, List <AllOpenShiftMappingEntity> lookUpEntriesFoundList, TeamToDepartmentJobMappingEntity mappedOrgJobEntity, List <AllOpenShiftMappingEntity> lookUpData) { var identicalOpenShifts = new List <AllOpenShiftMappingEntity>(); // Count up each individual open shift stored in our cache. var map = new Dictionary <string, int>(); foreach (var openShiftMapping in lookUpData) { if (map.ContainsKey(openShiftMapping.KronosOpenShiftUniqueId)) { map[openShiftMapping.KronosOpenShiftUniqueId] += openShiftMapping.KronosSlots; } else { map.Add(openShiftMapping.KronosOpenShiftUniqueId, openShiftMapping.KronosSlots); } } foreach (var item in map) { if (item.Value > 1) { // Where there are more than one identical entity in cache so we require seperate processing. identicalOpenShifts.AddRange(lookUpData.Where(x => x.KronosOpenShiftUniqueId == item.Key)); } } // Get each unique hash from the list of open shifts that occur more than once in cache. var identicalOpenShiftHash = identicalOpenShifts.Select(x => x.KronosOpenShiftUniqueId).Distinct(); foreach (var openShiftHash in identicalOpenShiftHash) { // Retrieve the open shifts to process from cache as well as what we have retrieved from Kronos using the hash. var openShiftsInCacheToProcess = identicalOpenShifts.Where(x => x.KronosOpenShiftUniqueId == openShiftHash).ToList(); var kronosOpenShiftsToProcess = openShiftsFoundList.Where(x => x.KronosUniqueId == openShiftHash).ToList(); // Calculate the difference in number of open shifts between Kronos and cache var cacheOpenShiftSlotCount = 0; openShiftsInCacheToProcess.ForEach(x => cacheOpenShiftSlotCount += x.KronosSlots); var numberOfOpenShiftsToRemove = cacheOpenShiftSlotCount - kronosOpenShiftsToProcess.Count; if (numberOfOpenShiftsToRemove > 0) { // More open shifts in cache than found in Kronos await this.RemoveAdditionalOpenShiftsFromTeamsAsync(allRequiredConfigurations, mappedOrgJobEntity, openShiftsInCacheToProcess, numberOfOpenShiftsToRemove).ConfigureAwait(false); continue; } if (numberOfOpenShiftsToRemove < 0) { // Less open shifts in cache than found in Kronos var openShiftsToAdd = new List <OpenShiftRequestModel>(); for (int i = numberOfOpenShiftsToRemove; i < 0; i++) { openShiftsToAdd.Add(kronosOpenShiftsToProcess.First()); } await this.CreateEntryOpenShiftsEntityMappingAsync(allRequiredConfigurations, openShiftsToAdd, lookUpEntriesFoundList, monthPartitionKey, mappedOrgJobEntity).ConfigureAwait(false); continue; } // The number of open shifts in Kronos and Teams matches meaning no action is needed continue; } }