This commit is contained in:
Zeeshaun
2026-03-13 09:24:21 -05:00
parent c2d6bda458
commit 256f579489
282 changed files with 50136 additions and 0 deletions
@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
@@ -0,0 +1,5 @@
[*]
indent_style = space
indent_size = 2
[*.{cs,csx,vb,vbx,razor}]
indent_size = 4
@@ -0,0 +1,446 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
#typescript Output
app.js
app.js.map
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
.idea/**
.vscode/**
@@ -0,0 +1,41 @@
# .NET Core SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS aspnetcore-build
# Install Node.js + npm (Node 18 LTS, required for TypeScript)
RUN apt-get update && apt-get install -y curl gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs
# Copy Projects
COPY LCU.WebApps.Internal.DocumentManager/LCU.WebApps.Internal.DocumentManager.csproj ./source/LCU.WebApps.Internal.DocumentManager/
# Install Credential Provider and set env variables to enable Nuget restore with auth
ARG PAT=vvqu3q252m5kqwptu4tavha4ghk4svhmwsahb2kvlu22ogiegt6q
RUN curl -L https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | sh
#RUN wget -qO- https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | bash
ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=true
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}"
# .NET Core Restore, use auth'd feed
RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" ./source/LCU.WebApps.Internal.DocumentManager/LCU.WebApps.Internal.DocumentManager.csproj
# Copy All Files
COPY . ./source/
# .NET Core Publish
FROM aspnetcore-build AS publish
RUN dotnet publish ./source/LCU.WebApps.Internal.DocumentManager/LCU.WebApps.Internal.DocumentManager.csproj -c Release -o /dist
# ASP.NET Core
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.20
RUN apk add --no-cache icu-libs && apk add --no-cache tzdata;
WORKDIR /app
COPY --from=publish /dist .
# ENVIRONMENT VALUES
ENV TZ=America/Chicago
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV ASPNETCORE_ENVIRONMENT=Development
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
ENTRYPOINT ["dotnet", "LCU.WebApps.Internal.DocumentManager.dll"]
@@ -0,0 +1,36 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.WebApps.Internal.DocumentManager", "LCU.WebApps.Internal.DocumentManager\LCU.WebApps.Internal.DocumentManager.csproj", "{4213EB34-DE40-4371-B8BB-CF5E0B0C6646}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F274295E-D19F-4FDF-8495-16F18AE2D8ED}"
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
.editorconfig = .editorconfig
.gitignore = .gitignore
azure-pipelines-iis.yml = azure-pipelines-iis.yml
azure-pipelines.yml = azure-pipelines.yml
Dockerfile = Dockerfile
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4213EB34-DE40-4371-B8BB-CF5E0B0C6646}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4213EB34-DE40-4371-B8BB-CF5E0B0C6646}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4213EB34-DE40-4371-B8BB-CF5E0B0C6646}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4213EB34-DE40-4371-B8BB-CF5E0B0C6646}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {03D039D2-36F6-4479-AA1F-96A8BFC61E70}
EndGlobalSection
EndGlobal
@@ -0,0 +1,18 @@
{
"version": 1,
"isRoot": true,
"tools": {
"microsoft.dotnet-msidentity": {
"version": "1.0.0",
"commands": [
"dotnet-msidentity"
]
},
"microsoft.dotnet-openapi": {
"version": "7.0.2",
"commands": [
"dotnet-openapi"
]
}
}
}
@@ -0,0 +1,16 @@
<CascadingAuthenticationState>
<CascadingBlazoredModal DisableBackgroundCancel="true">
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingBlazoredModal>
</CascadingAuthenticationState>
@@ -0,0 +1,133 @@
using LCU.WebApps.Internal.DocumentManager.Configurations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using System.Net.Http.Headers;
namespace LCU.WebApps.Internal.DocumentManager.Authentication;
public enum DownstreamWebService
{
DocumentTypeRetriever,
DocumentTypeUploader,
DocumentIngestor
}
public class AuthenticationHelper
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly AppConfiguration _appConfiguration;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
private readonly IAuthorizationService _authorizationService;
private const string ClientKeyName = "Client";
private const string ClientKeyValue = "DocumentManager";
private const string ApiKeyName = "ApiKey";
private readonly IDictionary<DownstreamWebService, string> _serviceScope;
public AuthenticationHelper
(
AuthenticationStateProvider authenticationStateProvider,
AppConfiguration appConfiguration,
ITokenAcquisition tokenAcquisition,
DocumentTypeRetrieverApiConfiguration documentTypeRetrieverConfiguration,
MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler,
IAuthorizationService authorizationService,
DocumentTypeUploaderApiConfiguration documentTypeUploaderApiConfiguration,
DocumentIngestorApiConfiguration documentIngestorApiConfiguration
)
{
_authenticationStateProvider = authenticationStateProvider;
_appConfiguration = appConfiguration;
_tokenAcquisition = tokenAcquisition;
_consentHandler = consentHandler;
_authorizationService = authorizationService;
_serviceScope = new Dictionary<DownstreamWebService, string>
{
[DownstreamWebService.DocumentTypeRetriever] = documentTypeRetrieverConfiguration.Scope,
[DownstreamWebService.DocumentTypeUploader] = documentTypeUploaderApiConfiguration.Scope,
[DownstreamWebService.DocumentIngestor] = documentIngestorApiConfiguration.Scope,
};
}
public HttpClient AddApiSecurityHeaders(HttpClient client)
{
if (!client.DefaultRequestHeaders.Contains(ClientKeyName))
{
client.DefaultRequestHeaders.Add(ClientKeyName, ClientKeyValue);
}
if (!client.DefaultRequestHeaders.Contains(ApiKeyName))
{
client.DefaultRequestHeaders.Add(ApiKeyName, _appConfiguration.DocumentManagerApiKey);
}
return client;
}
public async Task AddApiSecurityUserClaims(HttpClient client, DownstreamWebService service)
{
var token = await GetServiceToken(service);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
public async Task<string?> GetServiceToken(DownstreamWebService service)
{
async Task<string?> GetToken()
{
return await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { _serviceScope[service] });
}
try
{
return await GetToken();
}
catch (MsalUiRequiredException e)
{
_consentHandler.HandleException(e);
return null;
}
catch (MicrosoftIdentityWebChallengeUserException e)
{
_consentHandler.HandleException(e);
return null;
}
catch (Exception e)
{
throw new Exception($"Failure to authenticate downstream web service: {Enum.GetName(service)}."
+ $"Exception: {e.Message}");
}
}
public async Task<HttpClient> AddApiSecurityHeadersWithUserClaims(HttpClient client, DownstreamWebService service)
{
await AddApiSecurityUserClaims(client, service);
return AddApiSecurityHeaders(client);
}
public async Task<Dictionary<string, string>> CreateApiSecurityHeadersWithAuth(DownstreamWebService service)
{
var headers = new Dictionary<string, string>();
var token = await GetServiceToken(service);
headers.Add("Authorization", "Bearer " + token);
headers = CreateApiSecurityHeaders(headers);
return headers;
}
public Dictionary<string, string> CreateApiSecurityHeaders(Dictionary<string, string> headers)
{
headers.Add(ClientKeyName, ClientKeyValue);
headers.Add(ApiKeyName, _appConfiguration.DocumentManagerApiKey);
return headers;
}
public bool PolicySatisfied(string policy)
=> _authorizationService
.AuthorizeAsync(_authenticationStateProvider.GetAuthenticationStateAsync().Result.User, policy)
.Result.Succeeded;
}
@@ -0,0 +1,14 @@
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class AppConfiguration
{
public string AppName { get; set; }
public string SeqUrl { get; set; }
public string SeqApiKey { get; set; }
public string AppBasePath { get; set; }
public string QCRoles { get; set; }
public string ITRoles { get; set; }
public string DocumentManagerApiKey { get; set; }
public string UnknownEnabledStatuses { get; set; }
public DefaultFilters DefaultFilters { get; set; } = new();
}
@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class CoreApiWrapperConfiguration
{
public string Url { get; set; }
public string GetPersonEndpoint { get; set; }
public string GetPersonsByNameLike { get; set; }
public string GetCountPersonsByNameLike { get; set; }
public string GetOrganizationEndpoint { get; set; }
public string GetOrganizationByNameEndpoint { get; set; }
public string GetOwnersByAccountNumber { get; set; }
public string CountOrganizationNumbersByName { get; set; }
public string GetPersonOrOrganizationNumberByMemberId { get; set; }
[NotMapped]
public string GetPersonEndpointUrl => Url + GetPersonEndpoint;
[NotMapped]
public string GetOrganizationEndpointUrl => Url + GetOrganizationEndpoint;
[NotMapped]
public string GetOrganizationByNameEndpointUrl => Url + GetOrganizationByNameEndpoint;
[NotMapped]
public string GetOwnersByAccountNumberUrl => Url + GetOwnersByAccountNumber;
[NotMapped]
public string CountOrganizationNumbersByNameUrl => Url + CountOrganizationNumbersByName;
[NotMapped]
public string GetPersonsByNameLikeUrl => Url + GetPersonsByNameLike;
[NotMapped]
public string GetCountPersonsByNameLikeUrl => Url + GetCountPersonsByNameLike;
[NotMapped]
public string GetPersonOrOrganizationNumberByMemberIdUrl => Url + GetPersonOrOrganizationNumberByMemberId;
}
@@ -0,0 +1,11 @@
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class DefaultFilters
{
public string QCStatusFilters { get; set; }
public string ITStatusFilters { get; set; }
public int CreationDateStartOffsetDays { get; set; }
public int CreationDateEndOffsetDays { get; set; }
public int IngestionDateStartOffsetDays { get; set; }
public int IngestionDateEndOffsetDays { get; set; }
}
@@ -0,0 +1,50 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class DocumentIngestorApiConfiguration
{
public string Url { get; set; }
public string Scope { get; set; }
public int QueueReprocessDocumentAllCriticalFailuresTimeoutSeconds { get; set; }
public int QueueReprocessCompletionAllNotificationDataFailuresTimeoutSeconds { get; set; }
public string FilteredDocumentProcessingInfoEndpoint { get; set; }
public string QueueReprocessDocumentByIdEndpoint { get; set; }
public string QueueReprocessDocumentWithOverrideByIdEndpoint { get; set; }
public string QueueReprocessDocumentAllCriticalFailuresEndpoint { get; set; }
public string SetDocumentsToEntityUnknownEndpoint { get; set; }
public string QueueReprocessCompletionAllNotificationDataFailures { get; set; }
public string UpdateStatusEndpoint { get; set; }
public string SetDocumentNotificationStatus { get; set; }
public string BulkSetDocumentNotificationStatus { get; set; }
public string GetDocumentProcessingInfoByDocumentId { get; set; }
[NotMapped]
public string FilteredDocumentProcessingInfoEndpointUrl => Url + FilteredDocumentProcessingInfoEndpoint;
[NotMapped]
public string QueueReprocessDocumentByIdEndpointUrl => Url + QueueReprocessDocumentByIdEndpoint;
[NotMapped]
public string QueueReprocessDocumentWithOverrideByIdEndpointUrl => Url + QueueReprocessDocumentWithOverrideByIdEndpoint;
[NotMapped]
public string QueueReprocessDocumentAllCriticalFailuresEndpointUrl => Url + QueueReprocessDocumentAllCriticalFailuresEndpoint;
[NotMapped]
public string SetDocumentsToEntityUnknownEndpointUrl => Url + SetDocumentsToEntityUnknownEndpoint;
[NotMapped]
public string QueueReprocessCompletionAllNotificationDataFailuresUrl => Url + QueueReprocessCompletionAllNotificationDataFailures;
[NotMapped]
public string UpdateStatusEndpointUrl => Url + UpdateStatusEndpoint;
[NotMapped]
public string SetDocumentNotificationStatusUrl => Url + SetDocumentNotificationStatus;
[NotMapped]
public string BulkSetDocumentNotificationStatusUrl => Url + BulkSetDocumentNotificationStatus;
[NotMapped]
public string GetDocumentProcessingInfoByDocumentIdEndpointUrl => Url + GetDocumentProcessingInfoByDocumentId;
}
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class DocumentTypeRetrieverApiConfiguration
{
public string Url { get; set; }
public string Scope { get; set; }
public string DocumentsRoute { get; set; }
public string CountDocumentsRoute { get; set; }
public string DocumentContentRoute { get; set; }
public string DocumentActionRoute { get; set; }
public string DocumentAuditRoute { get; set; }
public string DocCodeDetailsEndpoint { get; set; }
public string DocCodeDetailsAuditEndpoint { get; set; }
[NotMapped]
public string Documents => Url + DocumentsRoute;
[NotMapped]
public string CountDocuments => Url + CountDocumentsRoute;
[NotMapped]
public string DocumentContent => Url + DocumentContentRoute;
[NotMapped]
public string DocumentAction => Url + DocumentActionRoute;
[NotMapped]
public string DocumentAudit => Url + DocumentAuditRoute;
[NotMapped]
public string DocCodeDetailsEndpointUrl => Url + DocCodeDetailsEndpoint;
[NotMapped]
public string DocCodeDetailsAuditEndpointUrl => Url + DocCodeDetailsAuditEndpoint;
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class DocumentTypeUploaderApiConfiguration
{
public string Url { get; set; }
public string Scope { get; set; }
public string AccountDocumentMetadataUpdateRoute { get; set; }
public string PrimaryOwnerDocumentMetadataUpdateRoute { get; set; }
[NotMapped]
public string AccountDocumentMetadataUpdate => Url + AccountDocumentMetadataUpdateRoute;
[NotMapped]
public string PrimaryOwnerDocumentMetadataUpdate => Url + PrimaryOwnerDocumentMetadataUpdateRoute;
}
@@ -0,0 +1,7 @@
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public class FileOperationsApiConfiguration
{
public string Url { get; set; }
public string GetPdfContentEndpoint { get; set; }
}
@@ -0,0 +1,27 @@
namespace LCU.WebApps.Internal.DocumentManager.Configurations;
public record Security(AccessPolicy[] Policies);
public record AccessPolicy(string Name, string[] Groups, AccessPolicy[]? Policies);
public static class SecurityConfigurationFactory
{
public static Security FromConfiguration(IConfigurationSection configuration)
{
var policies = configuration
.GetSection("AccessPolicies")
.GetChildren()
.Select(PolicyFactory);
return new Security(policies.ToArray());
}
private static AccessPolicy PolicyFactory(IConfigurationSection configuration)
{
return new AccessPolicy
(
configuration.Key,
configuration.GetValue<string>(nameof(AccessPolicy.Groups)).Split(",").Select(g => g.Trim()).ToArray(),
configuration.GetChildren().Where(c => c[nameof(AccessPolicy.Groups)] is not null).Select(PolicyFactory).ToArray()
);
}
}
@@ -0,0 +1,11 @@
namespace LCU.WebApps.Internal.DocumentManager.Enums;
public enum OwnershipTypes
{
MemberNumber,
AccountNumber,
PersonName,
OrganizationName,
OrganizationNumber,
PersonNumber
}
@@ -0,0 +1,86 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TypeScriptOutDir></TypeScriptOutDir>
<TypeScriptOutFile>wwwroot/scripts/app.js</TypeScriptOutFile>
<TypeScriptNoEmitOnError>True</TypeScriptNoEmitOnError>
<TypeScriptTarget>ES2020</TypeScriptTarget>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TypeScriptOutDir></TypeScriptOutDir>
<TypeScriptOutFile>wwwroot/scripts/app.js</TypeScriptOutFile>
<TypeScriptNoEmitOnError>True</TypeScriptNoEmitOnError>
<TypeScriptTarget>ES2020</TypeScriptTarget>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-LCU.WebApps.Internal.DocumentManager-6725618A-CB98-4C5B-9961-C82B04251A10</UserSecretsId>
<WebProject_DirectoryAccessLevelKey>0</WebProject_DirectoryAccessLevelKey>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<TypeScriptModuleKind>none</TypeScriptModuleKind>
<TypeScriptAllowJS>true</TypeScriptAllowJS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.FluentValidation" Version="2.1.0" />
<PackageReference Include="Blazored.Modal" Version="7.1.0" />
<PackageReference Include="Blazored.Toast" Version="3.2.2" />
<PackageReference Include="LCU.Library.DocumentType" Version="1.12.5" />
<PackageReference Include="LCU.Library.Internal.CoreApiWrapperModel" Version="2.27.3" />
<PackageReference Include="LCU.Library.Internal.DocumentIngestorModels" Version="1.20.1" />
<PackageReference Include="LCU.Library.Internal.DocumentModels" Version="1.8.0" />
<PackageReference Include="LCU.Library.Internal.FileOperationsModels" Version="4.1.0" />
<PackageReference Include="LCU.Library.Internal.LcuHttpClient" Version="2.0.1" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="1.1.0" />
<PackageReference Include="LCU.Library.Internal.RazorComponents" Version="2.3.0" />
<PackageReference Include="LCU.Library.Internal.Security" Version="2.0.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.11.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="3.11.0" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.8.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.18.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Diagnostics.FileVersionInfo" Version="4.3.0" />
<PackageReference Include="LCU.Library.Internal.Configuration" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="Shared\JSInterop\*.ts" />
</ItemGroup>
<ItemGroup>
<None Remove="Shared\DocumentGridView\DocumentAttributes.razor.css" />
<None Remove="Shared\JSInterop\JSTooltips.js" />
</ItemGroup>
<ItemGroup>
<Content Include="Shared\DocumentGridView\DocumentAttributes.razor.css" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\scripts\" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="Shared\JSInterop\JSTooltips.js" />
</ItemGroup>
<ItemGroup>
<OpenApiReference Include="DocumentTypeRetriever/spec.json" SourceUrl="https://qa-apps.landmarkcu.com/documenttyperetriever/swagger/v1/swagger.json">
<Namespace>DocumentTypeRetriever</Namespace>
<ClassName>Client</ClassName>
<OutputPath>DocumentTypeRetriever.Client.g.cs</OutputPath>
<Options>/DateTimeType:System.DateTime /GenerateClientInterfaces:true</Options>
</OpenApiReference>
</ItemGroup>
</Project>
@@ -0,0 +1,37 @@
using LCU.WebApps.Internal.DocumentManager.Pages;
namespace LCU.WebApps.Internal.DocumentManager.Models.Constants;
public static class PolicyConstants
{
public static readonly List<string> Policies = new()
{
Admin,
DocumentLookup,
DocumentLookupDownload,
DocumentLookupEditVisibility,
DocumentLookupShowDocumentId,
DocCodeDetails,
DocCodeDetailsCreate,
DocCodeDetailsEdit,
ReprocessingTable,
ReprocessingTableAssignPrimaryOwner,
ReprocessingTableOpenDocument,
ITReprocessingTable,
ITReprocessingTableReprocess
};
public const string Admin = "Admin";
public const string DocumentLookup = "DocumentLookup";
public const string DocumentLookupDownload = "DocumentLookupDownload";
public const string DocumentLookupEditVisibility = "DocumentLookupEditVisibility";
public const string DocumentLookupShowDocumentId = "DocumentLookupShowDocumentId";
public const string DocCodeDetails = "DocCodeDetails";
public const string DocCodeDetailsCreate = "DocCodeDetailsCreate";
public const string DocCodeDetailsEdit = "DocCodeDetailsEdit";
public const string ReprocessingTable = "ReprocessingTable";
public const string ReprocessingTableAssignPrimaryOwner = "ReprocessingTableAssignPrimaryOwner";
public const string ReprocessingTableOpenDocument = "ReprocessingTableOpenDocument";
public const string ITReprocessingTable = "ITReprocessingTable";
public const string ITReprocessingTableReprocess = "ITReprocessingTableReprocess";
}
@@ -0,0 +1,71 @@
using DocumentTypeRetriever;
using DocumentMetadataResponse = LCU.Library.DocumentType.Models.Responses.External.DocumentMetadataResponse;
namespace LCU.WebApps.Internal.DocumentManager.Models;
public static class DocumentCodeDetailsExcludeMemberViewExtension
{
public static string ExcludeMemberView(this DocumentCodeDetailsResponse documentCodeDetail)
{
const string dateFormat = "MM/dd/yyyy";
if (documentCodeDetail.DisableMemberView)
{
return "All";
}
if (documentCodeDetail.DisableMemberViewStartDate is null && documentCodeDetail.DisableMemberViewEndDate is null)
{
return "None";
}
if (documentCodeDetail.DisableMemberViewStartDate is null)
{
return "Error";
}
if (documentCodeDetail.DisableMemberViewEndDate is null)
{
return $"{documentCodeDetail.DisableMemberViewStartDate.Value.ToString(dateFormat)} - Forever";
}
if (documentCodeDetail.DisableMemberViewStartDate is not null && documentCodeDetail.DisableMemberViewEndDate is not null)
{
return $"{documentCodeDetail.DisableMemberViewStartDate.Value.ToString(dateFormat)} - {documentCodeDetail.DisableMemberViewEndDate.Value.ToString(dateFormat)}";
}
return string.Empty;
}
public static bool IsDisabledInMemberView(this DocumentCodeDetailsResponse documentCodeDetail,
DocumentMetadataResponse document)
{
if (documentCodeDetail.DisableMemberView)
{
return true;
}
if (documentCodeDetail.DisableMemberViewStartDate is null && documentCodeDetail.DisableMemberViewEndDate is null)
{
return false;
}
if (documentCodeDetail.DisableMemberViewStartDate is null)
{
return false;
}
if (documentCodeDetail.DisableMemberViewEndDate is null)
{
return documentCodeDetail.DisableMemberViewStartDate.Value.ToUniversalTime() <= document.CreatedDate;
}
if (documentCodeDetail.DisableMemberViewStartDate is not null && documentCodeDetail.DisableMemberViewEndDate is not null)
{
return documentCodeDetail.DisableMemberViewStartDate.Value.ToUniversalTime() <= document.CreatedDate
&& documentCodeDetail.DisableMemberViewEndDate.Value.ToUniversalTime() >= document.CreatedDate;
}
return false;
}
}
@@ -0,0 +1,64 @@
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace LCU.WebApps.Internal.DocumentManager.Models;
public class OverrideState
{
public OverrideState(int documentProcessingInfoId)
{
SelectedEntityType = "";
DnaResults = "";
OwnerOverrideInfo = new OwnerOverrideInfo
{
DocumentProcessingInfoId = documentProcessingInfoId,
EntityId = ""
};
}
public OwnerOverrideInfo OwnerOverrideInfo { get; set; }
public string SelectedEntityType { get; set; }
public string DnaResults { get; set; }
[NotMapped]
public bool IsEntityIdDisabled => string.IsNullOrWhiteSpace(SelectedEntityType) ||
SelectedEntityType == @Enum.GetName(typeof(EntityTypes), EntityTypes.Unknown);
[NotMapped]
public bool IsDnaLookUpDisabled => string.IsNullOrWhiteSpace(OwnerOverrideInfo.EntityId);
[NotMapped]
public bool PersonOrganizationAndEntityIdFilled => !string.IsNullOrWhiteSpace(OwnerOverrideInfo.EntityId) &&
(OwnerOverrideInfo.EntityType == EntityTypes.Person ||
OwnerOverrideInfo.EntityType == EntityTypes.Organization);
[NotMapped]
public bool HasDnaResult => !string.IsNullOrWhiteSpace(DnaResults);
[NotMapped]
public bool IsEntityTypeEmpty => !string.IsNullOrWhiteSpace(SelectedEntityType);
[NotMapped]
public bool ValuesSetForOverrideAndReprocess
{
get
{
var canOverrideAndReprocess = false;
if (IsEntityTypeEmpty)
{
OwnerOverrideInfo.EntityType = GetEntityTypes(SelectedEntityType);
canOverrideAndReprocess = (HasDnaResult && PersonOrganizationAndEntityIdFilled) ||
OwnerOverrideInfo.EntityType == EntityTypes.Unknown;
}
return canOverrideAndReprocess;
}
}
private EntityTypes GetEntityTypes(string entityTypeString)
{
return entityTypeString switch
{
nameof(EntityTypes.Unknown) => EntityTypes.Unknown,
nameof(EntityTypes.Organization) => EntityTypes.Organization,
_ => EntityTypes.Person
};
}
}
@@ -0,0 +1,71 @@
@page "/DocCodeDetails"
@using DocumentTypeRetriever;
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@attribute [Authorize(PolicyConstants.DocCodeDetails)]
@inject IDocumentCodeDetailsTableViewModel ViewModel;
@inject AuthenticationHelper AuthenticationHelper;
<div class="container-fluid">
<div class="row mt-4">
<h4>Doc Code Details Table</h4>
</div>
<TableFilter ViewModel="@ViewModel">
<FilterControls>
<DocumentCodeDetailsFilterControls Filter="@ViewModel.Filter"/>
</FilterControls>
<SortControls>
<TableSort ViewModel="@ViewModel"/>
</SortControls>
<AdditionalControls>
<DocCodeDetailsCreateButton CreateDocumentCodeDetails="@ViewModel.CreateDocumentCodeDetails"/>
</AdditionalControls>
</TableFilter>
@if (ViewModel.Loaded)
{
<DataTable ViewModel=@ViewModel T="DocumentCodeDetails " U="DocumentCodeDetailsResponse ">
<Row>
<DocumentCodeDetailsHeaderRow DocumentCodeDetail="@context.Details" />
</Row>
<RowContent>
<DocumentCodeDetailsRowBody
DocumentCodeDetail="@context.data.Details"
DocumentCodeDetailEdit="@context.pendingEdit"
SavePendingEdit="@context.savePendingEdit"
RefreshCodeDetails="@ViewModel.Refresh"
SetCollapsed="@context.setCollapsed"
EditingAllowed="@ViewModel.IsUserAuthorizedToEdit"
/>
<DocumentCodeDetailsAuditLog AuditTrail="@context.data.Logs"/>
</RowContent>
</DataTable>
<TablePaging ViewModel="@ViewModel"/>
}
else
{
<CardDisplay>
<div class="p-5">
<h3>No document data has been loaded...</h3>
</div>
</CardDisplay>
}
</div>
@code {
protected override async void OnInitialized()
{
await AuthenticationHelper.GetServiceToken(DownstreamWebService.DocumentTypeRetriever);
ViewModel.VisibleItems.CollectionChanged += (_, _) =>
{
if (ViewModel.VisibleItems.Count == ViewModel.VisibleItemsCount)
{
StateHasChanged();
}
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender) await ViewModel.Refresh();
}
}
@@ -0,0 +1,42 @@
@page
@model LCU.WebApps.Internal.DocumentManager.Pages.ErrorModel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Error</title>
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
</head>
<body>
<div class="main">
<div class="content px-4">
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Diagnostics;
namespace LCU.WebApps.Internal.DocumentManager.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}
@@ -0,0 +1,122 @@
@page "/ITReprocessingTable"
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@attribute [Authorize(PolicyConstants.ITReprocessingTable)]
@inject ITablePagingViewModel<DocumentProcessingInfo, Filter> ViewModel;
@inject AuthenticationHelper AuthenticationHelper;
<div class="container-fluid">
<div class="row mt-4">
<h4>IT Reprocessing Table</h4>
</div>
<TableFilter ViewModel="@ViewModel">
<FilterControls>
<DocumentProcessingInfoFilterControls Filter="@ViewModel.Filter">
<AdditionalFilters Context="filter">
<IngestorRunInfoIdFilterComponent Filter="@filter"/>
<CustomIndexFileFilter Filter="@filter"
Validate="@Validate"/>
<div class="form-row col-12 my-3">
<div class="col-6">
<DocumentProcessingInfoProcessStatusFilter Filter="@filter"/>
</div>
<div class="col-6">
<DocumentProcessingInfoNotificationStatusFilter Filter="@filter"/>
</div>
</div>
</AdditionalFilters>
</DocumentProcessingInfoFilterControls>
<FluentValidationValidator @ref="_fluentValidationValidator" Options="@(options => options.IncludeRuleSets(_ruleSets))"/>
</FilterControls>
<SortControls>
<TableSort ViewModel="@ViewModel"/>
</SortControls>
<AdditionalControls>
<div class="row justify-content-end">
<div class="mr-3">
<SetNotificationStatusButton Filter="@ViewModel.Filter"
RefreshData="@RefreshDataAndCloseModal"/>
</div>
<div class="mr-3">
<ReprocessNotificationDataFailuresButton/>
</div>
<div>
<ReprocessCriticalFailuresButton/>
</div>
</div>
</AdditionalControls>
</TableFilter>
@if (ViewModel.Loaded && ViewModel.VisibleItemsCount > 0)
{
<DataTable ViewModel=@ViewModel T="DocumentProcessingInfo " U="OverrideState ">
<Row>
<DocumentProcessingHeaderRow DocumentProcessingInfo="@context">
<AdditionalRowContent>
<NotificationStatusColumn DocumentProcessingInfo="@context"/>
</AdditionalRowContent>
</DocumentProcessingHeaderRow>
</Row>
<RowContent>
<ITReprocessingRowBody
DocumentProcessingInfo="@context.data"
OverrideState="@context.pendingEdit"
SavePendingEdit="@context.savePendingEdit"
RefreshData="@RefreshDataAndCloseModal"
SetCollapsed="@context.setCollapsed">
<AdditionalRowContent>
<NotificationChangeStatusContainer
DocumentProcessingInfo="@context.data"
RefreshData="@RefreshDataAndCloseModal"/>
<ChangeStatusContainer
DocumentProcessingInfo="@context.data"
RefreshData="@RefreshDataAndCloseModal"/>
</AdditionalRowContent>
</ITReprocessingRowBody>
</RowContent>
</DataTable>
<TablePaging ViewModel="@ViewModel"/>
}
else
{
<CardDisplay>
<div class="p-5">
<h3>
<span>No document data has been loaded</span>
</h3>
</div>
</CardDisplay>
}
</div>
@code {
private FluentValidationValidator? _fluentValidationValidator;
private readonly string[] _ruleSets = {"CustomIndexFile"};
protected override async Task OnInitializedAsync()
{
await AuthenticationHelper.GetServiceToken(DownstreamWebService.DocumentIngestor);
ViewModel.VisibleItems.CollectionChanged += (_, _) =>
{
if (ViewModel.VisibleItems.Count == ViewModel.VisibleItemsCount)
{
StateHasChanged();
}
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await ViewModel.Refresh();
}
private void RefreshDataAndCloseModal(IModalReference openModal)
{
ViewModel.Refresh();
openModal.Close();
}
private async Task Validate()
{
ViewModel.FilterIsValid = await _fluentValidationValidator!.ValidateAsync();
}
}
@@ -0,0 +1,89 @@
@page "/"
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@attribute [Authorize(PolicyConstants.DocumentLookup)]
@inject IBusinessViewViewModel ViewModel;
@inject AuthenticationHelper AuthenticationHelper;
<div class="container-fluid">
<div class="row mt-4">
<h4>Document Lookup</h4>
</div>
<TableFilter ViewModel="@ViewModel">
<FilterControls>
<BusinessViewFilterControls
Filter="@ViewModel.Filter"
OnOwnershipChange="@(change => ViewModel.FilterChanged(change.type, nameof(BusinessViewFilter.OwnershipType), change.value))"
OnAdditionalOwnershipChange="@(change => ViewModel.FilterChanged(change.type, nameof(BusinessViewFilter.AdditionalOwnershipValue), change.value))"/>
</FilterControls>
<SortControls>
<TableSort ViewModel="@ViewModel"/>
</SortControls>
</TableFilter>
@if (ViewModel.Loaded && !_dataLoadError && ViewModel.VisibleItemsCount > 0)
{
<DataTable ViewModel=@ViewModel T="BusinessViewDocumentInformation " U="object ">
<Row>
<BusinessViewDocumentHeaderRow Document="@context.DocumentInformation"/>
</Row>
<RowContent>
<BusinessViewDocumentRowBody Document="@context.data.DocumentInformation" ViewModel="@ViewModel"/>
<BusinessViewDocumentAuditTable AuditTrail="@context.data.Logs.OrderByDescending(x => x.Date)"/>
</RowContent>
</DataTable>
<TablePaging ViewModel="@ViewModel"/>
}
else
{
<CardDisplay>
<div class="p-5">
<h3>
@if (!_dataLoadError)
{
<span>No document data has been loaded</span>
}
else
{
<span class="text-danger">Something went wrong... Please check back later. If the problem persists contact IT.</span>
}
</h3>
</div>
</CardDisplay>
}
</div>
@code {
private bool _dataLoadError = false;
protected override async Task OnInitializedAsync()
{
await AuthenticationHelper.GetServiceToken(DownstreamWebService.DocumentTypeUploader);
await AuthenticationHelper.GetServiceToken(DownstreamWebService.DocumentTypeRetriever);
ViewModel.VisibleItems.CollectionChanged += (_, _) =>
{
if (ViewModel.VisibleItems.Count == ViewModel.VisibleItemsCount || ViewModel.VisibleItemsCount == 0)
{
_dataLoadError = false;
StateHasChanged();
}
};
ViewModel.DataLoadError += delegate
{
_dataLoadError = true;
StateHasChanged();
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await ViewModel.Refresh();
}
protected void RefreshDataAndCloseModal(IModalReference openModal)
{
ViewModel.Refresh();
openModal.Close();
}
}
@@ -0,0 +1,106 @@
@page "/ReprocessingTable"
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@attribute [Authorize(PolicyConstants.ReprocessingTable)]
@inject ITablePagingViewModel<DocumentProcessingInfo, Filter> ViewModel;
@inject AuthenticationHelper AuthenticationHelper;
<div class="container-fluid">
<div class="row mt-4">
<h4>Reprocessing Table</h4>
</div>
<TableFilter ViewModel="@ViewModel">
<FilterControls>
<DocumentProcessingInfoFilterControls Filter="@ViewModel.Filter"
Validate="@Validate">
<AdditionalFilters Context="filter">
<div class="form-row col-12">
<div class="col-12">
<DocumentProcessingInfoProcessStatusFilter Filter="@filter"
Validate="@Validate"/>
</div>
</div>
<div class="form-row col-12">
<DocumentProcessingInfoSearchCriteriaFilter Filter="@filter"
Validate="@Validate"/>
</div>
</AdditionalFilters>
</DocumentProcessingInfoFilterControls>
<FluentValidationValidator @ref="_fluentValidationValidator" Options="@(options => options.IncludeRuleSets(_ruleSets))"/>
</FilterControls>
<SortControls>
<TableSort ViewModel="@ViewModel"/>
</SortControls>
<AdditionalControls>
<div class="row justify-content-end">
<div>
<SetFilteredDocumentsEntityToUnknownButton ViewModel="@ViewModel"/>
</div>
</div>
</AdditionalControls>
</TableFilter>
@if (ViewModel.Loaded && ViewModel.VisibleItemsCount > 0)
{
<DataTable ViewModel=@ViewModel T="DocumentProcessingInfo " U="OverrideState ">
<Row>
<DocumentProcessingHeaderRow DocumentProcessingInfo="@context"/>
</Row>
<RowContent>
<BusinessReprocessingRowBody
DocumentProcessingInfo="@context.data"
OverrideState="@context.pendingEdit"
SavePendingEdit="@context.savePendingEdit"
RefreshData="@RefreshDataAndCloseModal"
SetCollapsed="@context.setCollapsed"/>
</RowContent>
</DataTable>
<TablePaging ViewModel="@ViewModel"/>
}
else
{
<CardDisplay>
<div class="p-5">
<h3>
<span>No document data has been loaded</span>
</h3>
</div>
</CardDisplay>
}
</div>
@code {
private FluentValidationValidator? _fluentValidationValidator;
private readonly string[] _ruleSets = {"SearchCriteria"};
protected override async Task OnInitializedAsync()
{
await AuthenticationHelper.GetServiceToken(DownstreamWebService.DocumentIngestor);
ViewModel.VisibleItems.CollectionChanged += (_, _) =>
{
if (ViewModel.VisibleItems.Count == ViewModel.VisibleItemsCount || ViewModel.VisibleItemsCount == 0)
{
StateHasChanged();
}
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await ViewModel.Refresh();
await Validate();
}
private async void RefreshDataAndCloseModal(IModalReference openModal)
{
await ViewModel.Refresh();
openModal.Close();
}
private async Task Validate()
{
ViewModel.FilterIsValid = await _fluentValidationValidator!.ValidateAsync();
}
}
@@ -0,0 +1,9 @@
@page "/"
@namespace LCU.WebApps.Internal.DocumentManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_Layout";
}
<base href="~/" />
<component type="typeof(App)" render-mode="ServerPrerendered" />
@@ -0,0 +1,49 @@
@using Microsoft.AspNetCore.Components.Web
@using LCU.WebApps.Internal.DocumentManager.Configurations
@namespace LCU.WebApps.Internal.DocumentManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject AppConfiguration _appConfiguration;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/@_appConfiguration.AppBasePath" />
<link href="css/site.css" rel="stylesheet" />
<link href="LCU.WebApps.Internal.DocumentManager.styles.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" />
<link href="_content/LCU.Library.Internal.RazorComponents/styles.css" rel="stylesheet" />
<link href="_content/LCU.Library.Internal.RazorComponents/bootstrap.min.css" rel="stylesheet" />
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
<link href="_content/Blazored.Typeahead/blazored-typeahead.css" rel="stylesheet" />
<link href="_content/LCU.Library.Internal.RazorComponents/grid.css" rel="stylesheet" />
<link href="_content/LCU.Library.Internal.RazorComponents/images/favicon.ico" rel="icon" type="image/x-icon" />
<link href="_content/LCU.Library.Internal.RazorComponents/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@RenderBody()
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
<script src="_content/Blazored.Modal/blazored.modal.js"></script>
<script src="_content/Blazored.Typeahead/blazored-typeahead.js"></script>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<script src="scripts/app.js"></script>
</body>
</html>
@@ -0,0 +1,139 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using Serilog;
using Blazored.Modal;
using Blazored.Toast;
using Blazored.Toast.Services;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Models;
using LCU.Library.Internal.Configuration;
using LCU.WebApps.Internal.DocumentManager.Authentication;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using LCU.WebApps.Internal.DocumentManager.Services;
using LCU.WebApps.Internal.DocumentManager.Services.Decorators;
using Microsoft.AspNetCore.Authorization;
using LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
using LCU.WebApps.Internal.DocumentManager.Shared.JSInterop;
using LCU.Library.Internal.Security;
using LCU.Library.Internal.LcuHttpClient;
var builder = BuildBuilder(args);
BuildServicesAndConfigurations(builder);
BuildAppAndRun(builder);
WebApplicationBuilder BuildBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("/app/config/environment-appsettings/appsettings.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/app/config/workload-appsettings/appsettings.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-manager-seq-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-manager-client-secret.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-manager-client-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-manager-api-key.json", optional: true, reloadOnChange: true);
builder.Host.UseSerilog();
if (builder.Environment.EnvironmentName.Contains("Local", StringComparison.OrdinalIgnoreCase))
{
builder.WebHost.UseStaticWebAssets();
}
return builder;
}
void BuildServicesAndConfigurations(WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddRazorPages();
// Add services to the container.
services.AddAzureAdGroupPolicyServicesWithToken(configuration.GetSection("AzureAd"),
configuration.GetSection("Security"));
services.AddControllersWithViews().AddMicrosoftIdentityUI();
services.AddServerSideBlazor().AddMicrosoftIdentityConsentHandler();
services.AddHttpClient();
services.AddLCUHttpServices();
services.AddHttpContextAccessor();
var appConfiguration = configuration.Bind<AppConfiguration>(services, "Primary");
configuration.Bind<CoreApiWrapperConfiguration>(services);
configuration.Bind<DocumentIngestorApiConfiguration>(services);
configuration.Bind<FileOperationsApiConfiguration>(services);
var documentTypeRetrieverConfiguration = configuration.Bind<DocumentTypeRetrieverApiConfiguration>(services);
configuration.Bind<DocumentTypeUploaderApiConfiguration>(services);
var loggerConfiguration = new LoggingConfiguration(appConfiguration.AppName,
appConfiguration.SeqApiKey,
appConfiguration.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
AddDocumentManagerServices(services);
AddBlazoreAdditionServices(services);
AddJavascriptInterop(services);
services.AddScoped<AuthenticationHelper>();
services.AddTransient<IDocumentCodeDetailsTableViewModel, DocumentCodeDetailsTableViewModel>();
services.AddTransient<ITablePagingViewModel<DocumentProcessingInfo, Filter>, DocumentProcessingInfoTableViewModel>();
services.AddTransient<IBusinessViewViewModel, BusinessViewViewModel>();
}
void AddDocumentManagerServices(IServiceCollection services)
{
services.AddScoped<DocumentProcessingInfoService>();
services.AddScoped<IDocumentProcessingInfoService>(p => new DocumentProcessingInfoServiceErrorDecorator(p.GetRequiredService<DocumentProcessingInfoService>(), p.GetRequiredService<IToastService>()));
services.AddScoped<QueueDocumentReprocessingService>();
services.AddScoped<IDocumentReprocessingService>(p => new DocumentReprocessingServiceErrorDecorator(p.GetRequiredService<QueueDocumentReprocessingService>(), p.GetRequiredService<IToastService>()));
services.AddScoped<CoreApiWrapperService>();
services.AddScoped<IEntityService>(p => new EntityServiceErrorDecorator(p.GetRequiredService<CoreApiWrapperService>(), p.GetRequiredService<IToastService>()));
services.AddScoped<DocumentTypeRetrieverService>();
services.AddScoped<IDocumentService>(p => new DocumentServiceErrorDecorator(p.GetRequiredService<DocumentTypeRetrieverService>(), p.GetRequiredService<IToastService>()));
services.AddScoped<IDocumentTypeService>(p => new DocumentTypeServiceErrorDecorator(p.GetRequiredService<DocumentTypeRetrieverService>(), p.GetRequiredService<IToastService>()));
services.AddScoped<DocumentTypeUploaderService>();
services.AddScoped<IDocumentTypeUploaderService>(p => new DocumentTypeUploaderServiceErrorDecorator(p.GetRequiredService<DocumentTypeUploaderService>(), p.GetRequiredService<IToastService>()));
services.AddScoped<IDocumentAuthorization, BusinessViewViewModel>();
}
void AddBlazoreAdditionServices(IServiceCollection services)
{
services.AddBlazoredToast();
services.AddBlazoredModal();
}
void AddJavascriptInterop(IServiceCollection services)
{
services.AddTransient<IJSDate, JSDate>();
services.AddTransient<IJSDocument, JSDocument>();
services.AddTransient<IJSTooltips, JSTooltips>();
}
void BuildAppAndRun(WebApplicationBuilder builder)
{
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
}
@@ -0,0 +1,44 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:31489",
"sslPort": 44347
}
},
"profiles": {
"Local-QA": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Local-QA"
},
"applicationUrl": "https://localhost:7141;http://localhost:5141",
"dotnetRunMessages": true
},
"Local-Local": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Local-Local"
},
"applicationUrl": "https://localhost:7141;http://localhost:5141",
"dotnetRunMessages": true
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
}
}
}
@@ -0,0 +1,183 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Accounts;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Mixed;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Organizations;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Persons;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using Newtonsoft.Json;
using Serilog;
using System.Collections.Specialized;
using System.Web;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public class CoreApiWrapperService : IEntityService
{
private readonly IGetRequest _getRequest;
private readonly CoreApiWrapperConfiguration _coreApiWrapperConfiguration;
public CoreApiWrapperService(IGetRequest getRequest,
CoreApiWrapperConfiguration coreApiWrapperConfiguration)
{
_getRequest = getRequest;
_coreApiWrapperConfiguration = coreApiWrapperConfiguration;
}
public async Task<IEnumerable<Person>> GetPersonByEntityNumber(string entityNumber)
{
try
{
var url = _coreApiWrapperConfiguration.GetPersonEndpointUrl.Replace("{personNumber}", entityNumber);
return await _getRequest.AsyncWithNewton<List<Person>>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Get Person By Entity Number. EntityNumber: {entityNumber}");
throw;
}
}
public async Task<IEnumerable<Person>?> GetPersonsByNameLike(PersonNameRequest request, int limit)
{
try
{
var uriString = EncodePersonNameRequestUri(request, limit);
var url = $"{_coreApiWrapperConfiguration.GetPersonsByNameLikeUrl}{uriString}";
return await _getRequest.AsyncWithNewton<List<Person>>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Get Persons By Name Like: PersonNameRequest. PersonNameRequest: {JsonConvert.SerializeObject(request)}. Limit: {limit}");
throw;
}
}
public async Task<int> GetCountPersonsByNameLike(PersonNameRequest request)
{
try
{
var uriString = EncodePersonNameRequestUri(request);
var url = $"{_coreApiWrapperConfiguration.GetCountPersonsByNameLikeUrl}{uriString}";
return await _getRequest.AsyncWithNewton<int>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Get Count Persons By Name Like: PersonNameRequest: {JsonConvert.SerializeObject(request)}");
throw;
}
}
public async Task<IEnumerable<Organization>> GetOrganizationByEntityNumber(string entityNumber)
{
try
{
var url = _coreApiWrapperConfiguration.GetOrganizationEndpointUrl.Replace("{organizationNumber}", entityNumber);
return await _getRequest.AsyncWithNewton<List<Organization>>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Get Organization By Entity Number. EntityNumber: {entityNumber}");
throw;
}
}
public async Task<IEnumerable<Organization>?> GetOrganizationByOrganizationName(string organizationName, int limit)
{
try
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["limit"] = limit.ToString();
var url = _coreApiWrapperConfiguration.GetOrganizationByNameEndpointUrl.Replace("{organizationName}", organizationName) + $"?{query}";
return await _getRequest.AsyncWithNewton<List<Organization>>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Get Organization By Organization Name. OrganizationName: {organizationName}");
throw;
}
}
private string EncodePersonNameRequestUri(PersonNameRequest request, int? limit = null)
{
NameValueCollection nameValueCollection = HttpUtility.ParseQueryString(string.Empty);
if (!string.IsNullOrWhiteSpace(request.FirstName))
{
nameValueCollection.Add("FirstName", request.FirstName);
}
if (!string.IsNullOrWhiteSpace(request.MdlName))
{
nameValueCollection.Add("MdlName", request.MdlName);
}
if (!string.IsNullOrWhiteSpace(request.LastName))
{
nameValueCollection.Add("LastName", request.LastName);
}
if (limit != null)
{
nameValueCollection.Add("limit", limit.ToString());
}
return $"?{nameValueCollection}";
}
public async Task<IEnumerable<AccountOwnership>> GetOwnersByAccountNumber(long accountNumber, DateTime creationDate)
{
try
{
var url = _coreApiWrapperConfiguration.GetOwnersByAccountNumberUrl;
var route = new UriBuilder(url);
var query = HttpUtility.ParseQueryString("");
query["accountNumber"] = accountNumber.ToString();
query["creationDate"] = creationDate.ToString();
route.Query = query.ToString();
return await _getRequest.AsyncWithNewton<List<AccountOwnership>>(route.ToString());
}
catch (Exception exception)
{
Log.Error(exception, $"Error Get AccountOwnership by account number. AccountNumber: {accountNumber}");
throw;
}
}
public async Task<long> CountOrganizationNumbersByName(string organizationName)
{
try
{
var url = _coreApiWrapperConfiguration.CountOrganizationNumbersByNameUrl.Replace("{organizationName}", organizationName);
return await _getRequest.AsyncWithNewton<long>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Counting Organization By Organization Name. OrganizationName: {organizationName}");
throw;
}
}
public async Task<PersonOrganization> GetPersonOrOrganizationNumberByMemberId(long memberId)
{
try
{
var url = _coreApiWrapperConfiguration.GetPersonOrOrganizationNumberByMemberIdUrl.Replace("{memberId}", memberId.ToString());
return await _getRequest.AsyncWithNewton<PersonOrganization>(url);
}
catch (Exception exception)
{
Log.Error(exception, $"Error Getting Member by Id. MemberId: {memberId}");
throw;
}
}
}
@@ -0,0 +1,58 @@
using Blazored.Toast.Services;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.Library.Internal.DocumentIngestorModels.Models;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public class DocumentProcessingInfoServiceErrorDecorator : ToastErrorServiceDecorator<IDocumentProcessingInfoService>, IDocumentProcessingInfoService
{
protected override string ServiceName => "Document Ingestor";
public DocumentProcessingInfoServiceErrorDecorator(IDocumentProcessingInfoService instance, IToastService toastService)
: base(instance, toastService)
{
}
public Task SetDocumentsToEntityUnknownByFilter(Filter filter, int totalItems)
=> TryAndToast
(
() => Instance.SetDocumentsToEntityUnknownByFilter(filter, totalItems),
"There was an issue setting entities to Unknown. Re-apply the filters and/or refresh the page. If failure continues, submit a ticket."
);
public Task SetDocumentToEntityUnknown(params int[] documentProcessingInfoIds)
=> TryAndToast
(
() => Instance.SetDocumentToEntityUnknown(documentProcessingInfoIds),
"Failure to set document entity to unknown. Please refresh page and if failure continues submit ticket."
);
public Task<PagedResponse<DocumentProcessingInfo>> GetFilteredDocumentProcessingInfos(Filter filter)
=> TryAndToast
(
() => Instance.GetFilteredDocumentProcessingInfos(filter),
"Failure to get Document Processing Information. Please refresh page and if failure continues submit ticket."
);
public Task<DocumentProcessingInfo> UpdateStatus(DocumentProcessingInfoStatusRequest request)
=> TryAndToast
(
() => Instance.UpdateStatus(request),
"Failure to Update Status. Please refresh page and if failure continues submit ticket."
);
public Task SetDocumentNotificationStatus(int documentProcessingInfoId, NotificationStatusTypes notificationStatus, string reason)
=> TryAndToast
(
() => Instance.SetDocumentNotificationStatus(documentProcessingInfoId, notificationStatus, reason),
"Failure to set notification status. Please refresh page and if failure continues submit ticket."
);
public Task BulkSetDocumentNotificationStatus(Filter filter, NotificationStatusTypes status, string reason)
=> TryAndToast
(
() => Instance.BulkSetDocumentNotificationStatus(filter, status, reason),
"Failure to batch set notification status. Please refresh page and if failure continues submit ticket."
);
}
@@ -0,0 +1,43 @@
using Blazored.Toast.Services;
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public class DocumentReprocessingServiceErrorDecorator : ToastErrorServiceDecorator<IDocumentReprocessingService>, IDocumentReprocessingService
{
protected override string ServiceName => "Document Ingestor";
public DocumentReprocessingServiceErrorDecorator(IDocumentReprocessingService instance, IToastService toastService)
: base(instance, toastService)
{
}
public Task ReprocessAllNotificationDataFailures()
=> TryAndToast
(
() => Instance.ReprocessAllNotificationDataFailures(),
$"There was an issue while queueing {nameof(ProcessStatusTypes.NotificationDataFailure)} documents. Please refresh page and if failure continues submit ticket."
);
public Task QueueDocumentReprocessingWithOverride(OwnerOverrideInfo ownerOverrideInfo)
=> TryAndToast
(
() => Instance.QueueDocumentReprocessingWithOverride(ownerOverrideInfo),
"Failure to queue document for reprocessing. Please refresh page and if failure continues submit ticket."
);
public Task QueueDocumentReprocessing(int documentProcessingInfoId)
=> TryAndToast
(
() => Instance.QueueDocumentReprocessing(documentProcessingInfoId),
"Failure to queue document for reprocessing. Please refresh page and if failure continues submit ticket."
);
public Task QueueDocumentReprocessingAllCriticalFailures()
=> TryAndToast
(
() => Instance.QueueDocumentReprocessingAllCriticalFailures(),
"There was an issue while queueing CriticalFailure documents. Please refresh page and if failure continues submit ticket."
);
}
@@ -0,0 +1,28 @@
using Blazored.Toast.Services;
using DocumentTypeRetriever;
using DocumentAction = LCU.Library.Internal.DocumentModels.Requests.DocumentAction;
using DocumentAuditResponse = LCU.Library.Internal.DocumentModels.Responses.DocumentAuditResponse;
using DocumentMetadataResponse = LCU.Library.DocumentType.Models.Responses.External.DocumentMetadataResponse;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public class DocumentServiceErrorDecorator : ToastErrorServiceDecorator<IDocumentService>, IDocumentService
{
protected override string ServiceName => "Document API";
public DocumentServiceErrorDecorator(IDocumentService instance, IToastService toastService)
: base(instance, toastService)
{
}
public Task<IEnumerable<DocumentMetadataResponse>> GetDocuments(DocumentInformationFilteredSearch filter)
=> TryAndToast(() => Instance.GetDocuments(filter));
public Task<long> CountDocuments(DocumentInformationFilter filter)
=> TryAndToast(() => Instance.CountDocuments(filter));
public Task<string> StreamDocumentContent(Guid documentBlobId)
=> TryAndToast(() => Instance.StreamDocumentContent(documentBlobId));
public Task ExternalActionPerformed(Guid documentId, DocumentAction action, string source)
=> TryAndToast(() => Instance.ExternalActionPerformed(documentId, action, source));
public Task<IEnumerable<DocumentAuditResponse>> GetDocumentLogs(Guid[] documentIds)
=> TryAndToast(() => Instance.GetDocumentLogs(documentIds));
}
@@ -0,0 +1,23 @@
using Blazored.Toast.Services;
using DocumentTypeRetriever;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public class DocumentTypeServiceErrorDecorator : ToastErrorServiceDecorator<IDocumentTypeService>, IDocumentTypeService
{
protected override string ServiceName => "Document Type Retriever";
public DocumentTypeServiceErrorDecorator(IDocumentTypeService instance, IToastService toastService)
: base(instance, toastService)
{
}
public Task<DocumentCodeDetailsResponse[]> GetDocumentTypes()
=> TryAndToast(() => Instance.GetDocumentTypes());
public Task<IEnumerable<DocumentCodeDetailsAuditResponse>> GetAuditLogsForDocumentCodeDetail(string documentCode)
=> TryAndToast(() => Instance.GetAuditLogsForDocumentCodeDetail(documentCode));
public Task<DocumentCodeDetailsResponse> CreateDocumentCodeDetail(DocumentCodeDetailsRequest documentCodeDetail)
=> TryAndToast(() => Instance.CreateDocumentCodeDetail(documentCodeDetail), $"There was an issue saving {documentCodeDetail.DocumentCode}");
public Task<DocumentCodeDetailsResponse> UpdateDocumentCodeDetails(DocumentCodeDetailsRequest request)
=> TryAndToast(() => Instance.UpdateDocumentCodeDetails(request), $"There was an error saving document code details for Doc Code: {request.DocumentCode}");
}
@@ -0,0 +1,23 @@
using Blazored.Toast.Services;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public class DocumentTypeUploaderServiceErrorDecorator : ToastErrorServiceDecorator<IDocumentTypeUploaderService>, IDocumentTypeUploaderService
{
protected override string ServiceName => "Document Type Uploader";
public DocumentTypeUploaderServiceErrorDecorator(IDocumentTypeUploaderService instance, IToastService toastService)
: base(instance, toastService)
{
}
public Task DocumentMetadataUpdate(DocumentInformation document, EntityTypes selectedEntityType,
string entityId)
=> TryAndToast
(
() => Instance.DocumentMetadataUpdate(document, selectedEntityType, entityId),
"There was an error changing authorization for document. If problem persists, contact the IT team."
);
}
@@ -0,0 +1,35 @@
using Blazored.Toast.Services;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Accounts;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Mixed;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Organizations;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Persons;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public class EntityServiceErrorDecorator : ToastErrorServiceDecorator<IEntityService>, IEntityService
{
protected override string ServiceName => "Core Api";
public EntityServiceErrorDecorator(IEntityService entityServiceImplementation, IToastService toastService)
: base(entityServiceImplementation, toastService)
{
}
public Task<IEnumerable<Person>> GetPersonByEntityNumber(string entityNumber)
=> TryAndToast(() => Instance.GetPersonByEntityNumber(entityNumber));
public Task<IEnumerable<Organization>> GetOrganizationByEntityNumber(string entityNumber)
=> TryAndToast(() => Instance.GetOrganizationByEntityNumber(entityNumber));
public Task<IEnumerable<AccountOwnership>> GetOwnersByAccountNumber(long accountNumber, DateTime creationDate)
=> TryAndToast(() => Instance.GetOwnersByAccountNumber(accountNumber, creationDate));
public Task<int> GetCountPersonsByNameLike(PersonNameRequest request)
=> TryAndToast(() => Instance.GetCountPersonsByNameLike(request));
public Task<IEnumerable<Person>?> GetPersonsByNameLike(PersonNameRequest request, int limit)
=> TryAndToast(() => Instance.GetPersonsByNameLike(request, limit));
public Task<long> CountOrganizationNumbersByName(string organizationName)
=> TryAndToast(() => Instance.CountOrganizationNumbersByName(organizationName));
public Task<IEnumerable<Organization>?> GetOrganizationByOrganizationName(string organizationName, int limit)
=> TryAndToast(() => Instance.GetOrganizationByOrganizationName(organizationName, limit));
public Task<PersonOrganization> GetPersonOrOrganizationNumberByMemberId(long memberId)
=> TryAndToast(() => Instance.GetPersonOrOrganizationNumberByMemberId(memberId));
}
@@ -0,0 +1,89 @@
using Blazored.Toast.Services;
namespace LCU.WebApps.Internal.DocumentManager.Services.Decorators;
public abstract class ToastErrorServiceDecorator<TDecorated>: IDisposable
{
protected readonly TDecorated Instance;
protected abstract string ServiceName { get; }
private readonly IList<string> _currentMessageStack = new List<string>();
private int _errorCount = 0;
private readonly IToastService _toastService;
private CancellationTokenSource _pendingErrorToast = new();
private readonly SemaphoreSlim _self = new (1, 1);
protected ToastErrorServiceDecorator(TDecorated instance, IToastService toastService)
{
Instance = instance;
_toastService = toastService;
}
protected async Task<T> TryAndToast<T>(Func<Task<T>> action, string? failureMessage = null)
{
try
{
return await action();
}
catch
{
QueueErrorToast(failureMessage);
throw;
}
}
protected async Task TryAndToast(Func<Task> action, string? failureMessage = null)
{
try
{
await action();
}
catch
{
QueueErrorToast(failureMessage);
throw;
}
}
private void QueueErrorToast(string? message = null)
{
_pendingErrorToast.Cancel();
var thisCancellation = _pendingErrorToast = new CancellationTokenSource();
if(message is not null) _currentMessageStack.Add(message);
_errorCount++;
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1), thisCancellation.Token);
if (thisCancellation.Token.IsCancellationRequested) return;
await _self.WaitAsync(TimeSpan.FromMilliseconds(500), thisCancellation.Token);
if (!thisCancellation.Token.IsCancellationRequested)
{
//todo ShowError takes a render fragment, would be great to show this as an html bulleted list
//initial attempts failed at rendering (breakpoint hit in the component but nothing rendered visibly)
//https://learn.microsoft.com/en-us/aspnet/core/blazor/advanced-scenarios?view=aspnetcore-7.0
var errorMessage = _currentMessageStack.Any()
? string.Join("\n", _currentMessageStack)
: $"{_errorCount} error(s) fetching data from {ServiceName}";
_toastService.ShowError(errorMessage);
_errorCount = default;
_currentMessageStack.Clear();
}
_self.Release();
}, thisCancellation.Token);
}
public void Dispose()
{
_pendingErrorToast.Dispose();
_self.Dispose();
}
}
@@ -0,0 +1,145 @@
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.Library.Internal.DocumentIngestorModels.Models;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.FileOperationsModels.Shared;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Library.Internal.Security.Service;
using LCU.WebApps.Internal.DocumentManager.Authentication;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using Serilog;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public class DocumentProcessingInfoService : IDocumentProcessingInfoService
{
private readonly IPostRequest _postRequest;
private readonly IPutRequest _putRequest;
private readonly DocumentIngestorApiConfiguration _documentIngestorApiConfiguration;
private readonly AuthenticationHelper _authenticationHelper;
private readonly IAzureAdAuthorizationService _azureAdAuthorizationService;
public DocumentProcessingInfoService(IPostRequest postRequest,
IPutRequest putRequest,
DocumentIngestorApiConfiguration documentIngestorApiConfiguration,
AuthenticationHelper authenticationHelper,
IAzureAdAuthorizationService azureAdAuthorizationService)
{
_postRequest = postRequest;
_putRequest = putRequest;
_documentIngestorApiConfiguration = documentIngestorApiConfiguration;
_authenticationHelper = authenticationHelper;
_azureAdAuthorizationService = azureAdAuthorizationService;
}
public async Task<PagedResponse<DocumentProcessingInfo>> GetFilteredDocumentProcessingInfos(Filter filter)
{
try
{
return await _postRequest.AsyncWithNewton<Filter, PagedResponse<DocumentProcessingInfo>>(
_documentIngestorApiConfiguration.FilteredDocumentProcessingInfoEndpointUrl,
filter);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting FilteredDocumentProcessingInfos.");
throw;
}
}
public async Task SetDocumentsToEntityUnknownByFilter(Filter filter, int totalItems)
{
var filterCopy = filter.DeepClone();
filterCopy.Limit = totalItems;
var pagedResponse = await _postRequest.AsyncWithNewton<Filter, PagedResponse<DocumentProcessingInfo>>(
_documentIngestorApiConfiguration.FilteredDocumentProcessingInfoEndpointUrl,
filterCopy);
if (totalItems != pagedResponse?.Items.Count())
{
var errorMessage = $"local memory holds {totalItems}, actual total items is now {pagedResponse?.Items.Count()}";
Log.Error(errorMessage);
throw new ExpiredDataException(errorMessage);
}
await SetDocumentToEntityUnknown(pagedResponse.Items.Select(x => x.DocumentProcessingInfoId).ToArray());
}
public async Task SetDocumentToEntityUnknown(params int[] documentProcessingInfoIds)
{
try
{
var entityUnknownRequest = new EntityUnknownRequest
{
UserName = _azureAdAuthorizationService.GetUserName(),
DocumentProcessingInfoIds = documentProcessingInfoIds.ToList()
};
await _postRequest.Async<EntityUnknownRequest>(new Uri(_documentIngestorApiConfiguration.SetDocumentsToEntityUnknownEndpointUrl), entityUnknownRequest);
}
catch (Exception exception)
{
Log.Error(exception, "Error setting document entity to unknown.");
throw;
}
}
public async Task<DocumentProcessingInfo> UpdateStatus(DocumentProcessingInfoStatusRequest request)
{
try
{
return await _postRequest.AsyncWithNewton<DocumentProcessingInfoStatusRequest, DocumentProcessingInfo>(
_documentIngestorApiConfiguration.UpdateStatusEndpointUrl,
request);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting FilteredDocumentProcessingInfos.");
throw;
}
}
public async Task SetDocumentNotificationStatus(int memberNotificationInfoId, NotificationStatusTypes notificationStatus, string reason)
{
try
{
var route = _documentIngestorApiConfiguration.SetDocumentNotificationStatusUrl
.Replace("{memberNotificationInfoId}", memberNotificationInfoId.ToString())
.Replace("{notificationStatus}", notificationStatus.ToString());
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentIngestor);
await _putRequest.Async<string>(new Uri(route), reason, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error setting document notification status.");
throw;
}
}
public async Task BulkSetDocumentNotificationStatus(Filter filter, NotificationStatusTypes status, string reason)
{
try
{
var route = _documentIngestorApiConfiguration.BulkSetDocumentNotificationStatusUrl;
var request = new BulkNotificationStatusRequest
{
Filter = filter,
NotificationStatus = status,
ReasonMessage = reason
};
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentIngestor);
await _putRequest.Async<BulkNotificationStatusRequest>(new Uri(route), request, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error setting batch document notification status.");
throw;
}
}
}
@@ -0,0 +1,215 @@
using DocumentTypeRetriever;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Library.Internal.Security.Service;
using LCU.WebApps.Internal.DocumentManager.Authentication;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using Serilog;
using System.Text;
using DocumentAction = LCU.Library.Internal.DocumentModels.Requests.DocumentAction;
using DocumentActionNotificationRequest = LCU.Library.Internal.DocumentModels.Requests.DocumentActionNotificationRequest;
using DocumentAuditResponse = LCU.Library.Internal.DocumentModels.Responses.DocumentAuditResponse;
using DocumentMetadataResponse = LCU.Library.DocumentType.Models.Responses.External.DocumentMetadataResponse;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public class DocumentTypeRetrieverService : IDocumentService, IDocumentTypeService
{
private readonly IGetRequest _getRequest;
private readonly IPostRequest _postRequest;
private readonly IPutRequest _putRequest;
private readonly DocumentTypeRetrieverApiConfiguration _configuration;
private readonly AuthenticationHelper _authenticationHelper;
private readonly IAzureAdAuthorizationService _azureAdAuthorizationService;
private const string UserIdHeaderKey = "userId";
public DocumentTypeRetrieverService
(
IGetRequest getRequest,
IPostRequest postRequest,
IPutRequest putRequest,
DocumentTypeRetrieverApiConfiguration configuration,
AuthenticationHelper authenticationHelper,
IAzureAdAuthorizationService azureAdAuthorizationService)
{
_getRequest = getRequest;
_postRequest = postRequest;
_putRequest = putRequest;
_configuration = configuration;
_authenticationHelper = authenticationHelper;
_azureAdAuthorizationService = azureAdAuthorizationService;
}
#region IDocumentTypeService
public async Task<DocumentCodeDetailsResponse[]> GetDocumentTypes()
{
try
{
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentTypeRetriever);
return await _getRequest.AsyncWithNewton<DocumentCodeDetailsResponse[]>(new Uri(_configuration.DocCodeDetailsEndpointUrl + "/Get"), headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting DocumentTypeResponse.");
throw;
}
}
public async Task<IEnumerable<DocumentCodeDetailsAuditResponse>> GetAuditLogsForDocumentCodeDetail(string documentCode)
{
try
{
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
var uri = new Uri(_configuration.DocCodeDetailsAuditEndpointUrl.Replace("{documentCode}", documentCode));
var responses = await _getRequest.AsyncWithNewton<List<DocumentCodeDetailsAuditResponse>>(uri, headers);
return responses.OrderByDescending(x => x.TimeCreated);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting DocumentCodeDetailsAuditResponse.");
throw;
}
}
public async Task<DocumentCodeDetailsResponse> CreateDocumentCodeDetail(DocumentCodeDetailsRequest documentCodeDetail)
{
try
{
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentTypeRetriever);
headers.Add(UserIdHeaderKey, _azureAdAuthorizationService.GetUserName());
return await _postRequest.AsyncWithNewton<DocumentCodeDetailsRequest, DocumentCodeDetailsResponse>(new Uri(_configuration.DocCodeDetailsEndpointUrl),
documentCodeDetail,
headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error Creating DocumentCodeDetail.");
throw;
}
}
public async Task<DocumentCodeDetailsResponse> UpdateDocumentCodeDetails(DocumentCodeDetailsRequest request)
{
try
{
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentTypeRetriever);
headers.Add(UserIdHeaderKey, _azureAdAuthorizationService.GetUserName());
return await _putRequest.AsyncWithNewton<DocumentCodeDetailsRequest, DocumentCodeDetailsResponse>(new Uri(_configuration.DocCodeDetailsEndpointUrl),
request,
headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error updating DocumentCodeDetailsRequest.");
throw;
}
}
#endregion
public async Task<IEnumerable<DocumentMetadataResponse>> GetDocuments(DocumentInformationFilteredSearch filter)
{
try
{
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
var uri = new Uri(_configuration.Documents);
return await _postRequest.AsyncWithNewton<DocumentInformationFilteredSearch, IEnumerable<DocumentMetadataResponse>>(uri, filter, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting filtered DocumentMetadataResponse.");
throw;
}
}
public async Task<long> CountDocuments(DocumentInformationFilter filter)
{
try
{
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
var uri = new Uri(_configuration.CountDocuments);
return await _postRequest.AsyncWithNewton<DocumentInformationFilter, long>(uri, filter, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error Counting Documents");
throw;
}
}
public async Task<string> StreamDocumentContent(Guid documentBlobId)
{
try
{
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
var uri = new Uri(_configuration.DocumentContent.Replace("{documentBlobId}", documentBlobId.ToString()));
return await _postRequest.AsyncWithNewton<string, string>(uri, "", headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting document stream content");
throw;
}
}
public async Task ExternalActionPerformed(Guid documentId, DocumentAction action, string source)
{
try
{
var request = new DocumentActionNotificationRequest
{
Action = action,
Source = source
};
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentTypeRetriever);
headers.Add(UserIdHeaderKey, _azureAdAuthorizationService.GetUserName());
var uri = new Uri(_configuration.DocumentAction.Replace("{documentId}", documentId.ToString()));
await _postRequest.Async<DocumentActionNotificationRequest>(uri, request, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error getting document stream content");
throw;
}
}
public async Task<IEnumerable<DocumentAuditResponse>> GetDocumentLogs(Guid[] documentIds)
{
if (!documentIds.Any())
{
return Array.Empty<DocumentAuditResponse>();
}
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
headers.Add(UserIdHeaderKey, _azureAdAuthorizationService.GetUserName());
const int maxQueryLength = 2048;
var queryParamByteLength = Encoding.ASCII.GetBytes($"&documentIds={Guid.Empty}").Length;
var requestCount = (int)Math.Ceiling((double)(documentIds.Length * queryParamByteLength) / maxQueryLength);
var idsPerRequest = (int)Math.Ceiling((double)(documentIds.Length / requestCount));
var splitQueries = new List<string>();
for (var i = 0; i < requestCount; i++)
{
splitQueries.Add($"?{string.Join('&', documentIds.Skip(i*idsPerRequest).Take((i+1)*idsPerRequest).Select(id => $"documentIds={id}"))}");
}
return (await Task.WhenAll(splitQueries.Select(async query =>
{
return await _getRequest.AsyncWithNewton<IEnumerable<DocumentAuditResponse>>($"{_configuration.DocumentAudit}{query}", headers);
}))
).SelectMany(l => l).OrderByDescending(l => l.Date);
}
}
@@ -0,0 +1,137 @@
using Newtonsoft.Json;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using LCU.WebApps.Internal.DocumentManager.Authentication;
using LCU.Library.DocumentType.Enums;
using System.Text;
using LCU.Library.DocumentType.Extensions;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
using LCU.Library.Internal.Security.Service;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using Serilog;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public class DocumentTypeUploaderService : IDocumentTypeUploaderService
{
private readonly IPutRequest _putRequest;
private readonly DocumentTypeUploaderApiConfiguration _configuration;
private readonly AuthenticationHelper _authenticationHelper;
private readonly IAzureAdAuthorizationService _azureAdAuthorizationService;
private const string UserIdHeaderKey = "userId";
public DocumentTypeUploaderService
(
IPutRequest putRequest,
DocumentTypeUploaderApiConfiguration configuration,
AuthenticationHelper authenticationHelper,
IAzureAdAuthorizationService azureAdAuthorizationService
)
{
_putRequest = putRequest;
_configuration = configuration;
_authenticationHelper = authenticationHelper;
_azureAdAuthorizationService = azureAdAuthorizationService;
}
private async Task AccountDocumentMetadataUpdate(Guid documentId, object request)
{
try
{
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentTypeUploader);
headers.Add(UserIdHeaderKey, _azureAdAuthorizationService.GetUserName());
var uri = new Uri(_configuration.AccountDocumentMetadataUpdate.Replace("{documentId}", documentId.ToString()));
await _putRequest.Async<object>(uri, request, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error during AccountDocumentMetadataUpdate");
throw;
}
}
private async Task PrimaryOwnerDocumentMetadataUpdate(Guid documentId, object request)
{
try
{
var headers = await _authenticationHelper.CreateApiSecurityHeadersWithAuth(DownstreamWebService.DocumentTypeUploader);
headers.Add(UserIdHeaderKey, _azureAdAuthorizationService.GetUserName());
var uri = new Uri(_configuration.PrimaryOwnerDocumentMetadataUpdate.Replace("{documentId}", documentId.ToString()));
await _putRequest.Async<object>(uri, request, headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error during AccountDocumentMetadataUpdate");
throw;
}
}
public async Task DocumentMetadataUpdate(DocumentInformation document, EntityTypes selectedEntityType, string entityId)
{
if (EnumExtensions.IsDocumentTypeOf<PrimaryOwnerDocumentTypes>(document.DocumentCode))
{
string personNumber = null;
string orgNumber = null;
if(selectedEntityType == EntityTypes.Person)
{
personNumber = entityId;
}
else
{
orgNumber = entityId;
}
var request = new
{
PrimaryOwnerAttribute = new
{
AccountNumber = document.AccountNumber.ToString(),
OrganizationNumber = orgNumber,
PersonNumber = personNumber
},
PrimaryOwnerDocumentMetadata = new
{
CreatedDate = document.CreatedDate,
DataSource = document.DataSource,
Source = document.Source,
Name = document.Name,
TraceId = document.TraceId,
Type = document.DocumentCode
}
};
await PrimaryOwnerDocumentMetadataUpdate(document.DocumentId, request);
}
else if (EnumExtensions.IsDocumentTypeOf<AccountDocumentTypes>(document.DocumentCode))
{
var request = new
{
AccountAttribute = new
{
AccountNumber = document.AccountNumber.ToString(),
OrganizationNumbers = document.OrganizationNumbers.ToArray(),
PersonNumbers = document.PersonNumbers.ToArray()
},
AccountDocumentMetadata = new
{
CreatedDate = document.CreatedDate,
DataSource = document.DataSource,
Source = document.Source,
Name = document.Name,
TraceId = document.TraceId,
Type = document.DocumentCode
}
};
await AccountDocumentMetadataUpdate(document.DocumentId, request);
}
else
{
throw new NotSupportedException($"document code {document.DocumentCode} does not support updates");
}
}
}
@@ -0,0 +1,9 @@
namespace LCU.WebApps.Internal.DocumentManager.Services;
public class ExpiredDataException : Exception
{
public ExpiredDataException(string message)
:base(message)
{
}
}
@@ -0,0 +1,16 @@
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.Library.Internal.DocumentIngestorModels.Models;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public interface IDocumentProcessingInfoService
{
Task SetDocumentsToEntityUnknownByFilter(Filter filter, int totalItems);
Task SetDocumentToEntityUnknown(params int[] documentProcessingInfoIds);
Task<PagedResponse<DocumentProcessingInfo>> GetFilteredDocumentProcessingInfos(Filter filter);
Task<DocumentProcessingInfo> UpdateStatus(DocumentProcessingInfoStatusRequest request);
Task SetDocumentNotificationStatus(int documentProcessingInfoId, NotificationStatusTypes notificationStatus, string reason);
Task BulkSetDocumentNotificationStatus(Filter filter, NotificationStatusTypes status, string reason);
}
@@ -0,0 +1,11 @@
using LCU.Library.Internal.DocumentIngestorModels.Entities;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public interface IDocumentReprocessingService
{
Task ReprocessAllNotificationDataFailures();
Task QueueDocumentReprocessingWithOverride(OwnerOverrideInfo ownerOverrideInfo);
Task QueueDocumentReprocessing(int documentProcessingInfoId);
Task QueueDocumentReprocessingAllCriticalFailures();
}
@@ -0,0 +1,15 @@
using DocumentTypeRetriever;
using DocumentAction = LCU.Library.Internal.DocumentModels.Requests.DocumentAction;
using DocumentAuditResponse = LCU.Library.Internal.DocumentModels.Responses.DocumentAuditResponse;
using DocumentMetadataResponse = LCU.Library.DocumentType.Models.Responses.External.DocumentMetadataResponse;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public interface IDocumentService
{
Task<IEnumerable<DocumentMetadataResponse>> GetDocuments(DocumentInformationFilteredSearch filter);
Task<long> CountDocuments(DocumentInformationFilter filter);
Task<string> StreamDocumentContent(Guid documentBlobId);
Task ExternalActionPerformed(Guid documentId, DocumentAction action, string source);
Task<IEnumerable<DocumentAuditResponse>> GetDocumentLogs(Guid[] documentIds);
}
@@ -0,0 +1,11 @@
using DocumentTypeRetriever;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public interface IDocumentTypeService
{
Task<DocumentCodeDetailsResponse[]> GetDocumentTypes();
Task<IEnumerable<DocumentCodeDetailsAuditResponse>> GetAuditLogsForDocumentCodeDetail(string documentCode);
Task<DocumentCodeDetailsResponse> CreateDocumentCodeDetail(DocumentCodeDetailsRequest documentCodeDetail);
Task<DocumentCodeDetailsResponse> UpdateDocumentCodeDetails(DocumentCodeDetailsRequest request);
}
@@ -0,0 +1,9 @@
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public interface IDocumentTypeUploaderService
{
Task DocumentMetadataUpdate(DocumentInformation document, EntityTypes selectedEntityType, string entityId);
}
@@ -0,0 +1,19 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Accounts;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Mixed;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Organizations;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Persons;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public interface IEntityService
{
public Task<IEnumerable<Person>> GetPersonByEntityNumber(string entityNumber);
public Task<IEnumerable<Organization>> GetOrganizationByEntityNumber(string entityNumber);
public Task<IEnumerable<AccountOwnership>> GetOwnersByAccountNumber(long accountNumber, DateTime creationDate);
Task<int> GetCountPersonsByNameLike(PersonNameRequest request);
Task<IEnumerable<Person>?> GetPersonsByNameLike(PersonNameRequest request, int limit);
Task<long> CountOrganizationNumbersByName(string organizationName);
Task<IEnumerable<Organization>?> GetOrganizationByOrganizationName(string organizationName, int limit);
Task<PersonOrganization> GetPersonOrOrganizationNumberByMemberId(long memberId);
}
@@ -0,0 +1,102 @@
using LCU.Library.Internal.DocumentIngestorModels.Entities;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Library.Internal.Security.Service;
using LCU.WebApps.Internal.DocumentManager.Authentication;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using Serilog;
namespace LCU.WebApps.Internal.DocumentManager.Services;
public class QueueDocumentReprocessingService : IDocumentReprocessingService
{
private readonly IPostRequest _postRequest;
private readonly DocumentIngestorApiConfiguration _documentIngestorApiConfiguration;
private readonly AuthenticationHelper _authenticationHelper;
private readonly IAzureAdAuthorizationService _azureAdAuthorizationService;
public QueueDocumentReprocessingService(IPostRequest postRequest,
DocumentIngestorApiConfiguration documentIngestorApiConfiguration,
AuthenticationHelper authenticationHelper,
IAzureAdAuthorizationService azureAdAuthorizationService)
{
_postRequest = postRequest;
_documentIngestorApiConfiguration = documentIngestorApiConfiguration;
_authenticationHelper = authenticationHelper;
_azureAdAuthorizationService = azureAdAuthorizationService;
}
public async Task QueueDocumentReprocessing(int documentProcessingInfoId)
{
try
{
var url = _documentIngestorApiConfiguration.
QueueReprocessDocumentByIdEndpointUrl.Replace("{documentProcessingInfoId}", documentProcessingInfoId.ToString())
.Replace("{userName}", _azureAdAuthorizationService.GetUserName());
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
await _postRequest.Async<string>(new Uri(url), "", headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error Queueing Document Processing Request." +
$"DocumentProcessingInfoId:{documentProcessingInfoId}");
throw;
}
}
public async Task QueueDocumentReprocessingWithOverride(OwnerOverrideInfo ownerOverrideInfo)
{
try
{
ownerOverrideInfo.UserName = _azureAdAuthorizationService.GetUserName();
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
await _postRequest.Async<OwnerOverrideInfo>(new Uri(_documentIngestorApiConfiguration.QueueReprocessDocumentWithOverrideByIdEndpointUrl),
ownerOverrideInfo,
headers);
}
catch (Exception exception)
{
Log.Error(exception, "Error Queue Document Reprocessing With Override. " +
$"DocumentProcessingInfoId:{ownerOverrideInfo.DocumentProcessingInfoId}");
throw;
}
}
public async Task QueueDocumentReprocessingAllCriticalFailures()
{
try
{
var uri = new Uri(_documentIngestorApiConfiguration.
QueueReprocessDocumentAllCriticalFailuresEndpointUrl.Replace("{userName}", _azureAdAuthorizationService.GetUserName()));
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
await _postRequest.Async<string>(uri, "", headers, 0, _documentIngestorApiConfiguration.QueueReprocessDocumentAllCriticalFailuresTimeoutSeconds);
}
catch (Exception exception)
{
Log.Error(exception, "Error Queueing Document Processing Request For All Critical Failures.");
throw;
}
}
public async Task ReprocessAllNotificationDataFailures()
{
try
{
var uri = new Uri(_documentIngestorApiConfiguration.
QueueReprocessCompletionAllNotificationDataFailuresUrl
.Replace("{userName}", _azureAdAuthorizationService.GetUserName()));
var headers = _authenticationHelper.CreateApiSecurityHeaders(new Dictionary<string, string>());
await _postRequest.Async<string>(uri, "", headers, 0, _documentIngestorApiConfiguration.QueueReprocessCompletionAllNotificationDataFailuresTimeoutSeconds);
}
catch (Exception exception)
{
Log.Error(exception, "Error Queueing Document Processing Request For All Data Failures.");
throw;
}
}
}
@@ -0,0 +1,11 @@
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
<AuthorizeView Policy="@PolicyConstants.Admin">
@ChildContent
</AuthorizeView>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@@ -0,0 +1,19 @@
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
<AdminComponent>
<div class="dropdown">
<a class="nav-link dropdown-toggle" role="button" id="dropdownMenuButton" data-toggle="dropdown" data-boundary="viewport" data-flip="true" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-cog mr-2" aria-hidden="true"></span> Admin
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<AuthorizeView Policy="@PolicyConstants.DocCodeDetails">
<a class="dropdown-item text-dark" href="DocCodeDetails">Doc Code Details Table</a>
</AuthorizeView>
<AuthorizeView Policy="@PolicyConstants.ReprocessingTable">
<a class="dropdown-item text-dark" href="ReprocessingTable">Reprocessing Table</a>
</AuthorizeView>
<AuthorizeView Policy="@PolicyConstants.ITReprocessingTable">
<a class="dropdown-item text-dark" href="ITReprocessingTable">IT Reprocessing Table</a>
</AuthorizeView>
</div>
</div>
</AdminComponent>
@@ -0,0 +1,95 @@
@inject IJSTooltips JSTooltips;
<div class="form-row col-12 mt-3">
<div class="col-6">
<div class="row">
<div class="col">
<label class="col-12 col-form-label">Custom Filter Key <i class="fa fa-info-circle" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="Advanced: you must know the exact index key and value for this filtering method to work."></i>:</label>
<InputText id="CustomFilter"
type="text"
class="form-control col-12"
placeholder="Type to add custom filter by key..."
@bind-Value="@_customFilterKey"
@oninput="@(change => { _customFilterKey = change.Value!.ToString(); StateHasChanged(); })"/>
</div>
</div>
<div class="row my-2">
<div class="col flex-end">
<button type="button"
class="btn btn-primary"
title="Add Filter"
@onclick="@AddCustomFilter"
disabled="@(string.IsNullOrWhiteSpace(_customFilterKey) || Filter.CustomIndexFileFiltersEquals.ContainsKey(_customFilterKey))">
Add Filter
</button>
</div>
</div>
</div>
<div class="col-6">
@foreach (var filter in Filter.CustomIndexFileFiltersEquals)
{
var key = filter.Key;
var value = filter.Value;
<div class="">
<label class="col-12 col-form-label">Custom Filter - @(key):</label>
<InputText type="text"
id="@key"
style="display: inline-block;"
class="col-11 form-control"
placeholder="Type to filter custom value..."
@bind-Value="@value"
@oninput="@(change => FilterHasChanged(key, change.Value!.ToString()))"/>
<i class="col-auto fa fa-times pr-0" style="cursor: pointer;" onclick="@(() => RemoveCustomFilter(key))"></i>
</div>
}
</div>
</div>
<span class="col-12 col-form-label" style="display:inline-block;">
<ValidationMessage For="@(() => Filter.CustomIndexFileFiltersEquals)"/>
</span>
@code {
[Parameter]
public Filter Filter { get; set; }
[Parameter]
public EventCallback Validate { get; set; }
private string _customFilterKey = "";
private async void ValidateForm() => await Validate.InvokeAsync();
protected override void OnParametersSet()
{
Filter.CustomIndexFileFiltersEquals ??= new Dictionary<string, string>();
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) JSTooltips.Enable();
return base.OnAfterRenderAsync(firstRender);
}
private void AddCustomFilter()
{
Filter.CustomIndexFileFiltersEquals[_customFilterKey] = string.Empty;
_customFilterKey = string.Empty;
ValidateForm();
}
private void RemoveCustomFilter(string key)
{
Filter.CustomIndexFileFiltersEquals.Remove(key);
ValidateForm();
StateHasChanged();
}
private void FilterHasChanged(string key, string value)
{
Filter.CustomIndexFileFiltersEquals[key] = value;
ValidateForm();
}
}
@@ -0,0 +1,130 @@
@using LCU.WebApps.Internal.DocumentManager.Enums;
<div class="form-row col-12">
<div class="col-6" style="padding-left: 4px;">
<label class="col-12 col-form-label text-nowrap">Doc Creation Date Start:</label>
<InputDate id="CreatedDateStart" class="form-control" TValue="DateTime?" @bind-Value="@Filter.DocumentProcessingFilter.Filter.CreatedDateMinimum"/>
</div>
<div class="col-6" style="padding-left: 4px;">
<label class="col-12 col-form-label text-nowrap">Doc Creation Date End:</label>
<InputDate id="CreatedDateEnd" class="form-control" TValue="DateTime?" Value="@Filter.DocumentProcessingFilter.Filter.CreatedDateMaximum" ValueExpression="@(() => Filter.DocumentProcessingFilter.Filter.CreatedDateMaximum)" ValueChanged="@SetMaxInclusive"/>
</div>
</div>
<div class="form-row col-12">
<div class="col-6">
<label class="col-12 col-form-label">
<span class="text-nowrap">Ownership Criteria:</span>
</label>
<BlazoredTypeahead SearchMethod="@FilterMemberExclusions"
TValue="@string"
TItem="@string"
Value="@SelectedOption"
ValueExpression="@(() => SelectedOption)"
ValueChanged="@MemberExclusionTypeSelected"
EnableDropDown="true"
placeholder="Select an option...">
<SelectedTemplate>
@context
</SelectedTemplate>
<ResultTemplate>
@context
</ResultTemplate>
</BlazoredTypeahead>
</div>
<div class="col-6">
<label class="col-12 col-form-label">&nbsp;</label>
@if (Filter?.OwnershipType == OwnershipTypes.PersonName)
{
<InputText type="search"
id="DocTypeFilter"
@bind-Value="@Filter.OwnershipValue"
@oninput="@(i => OnOwnershipChange.InvokeAsync((Filter.OwnershipType, i.Value.ToString())))"
class="form-control"
placeholder="First Name"
spellcheck="false"
autocomplete="false"/>
<InputText type="search"
id="DocTypeFilterAdditional"
@bind-Value="@Filter.AdditionalOwnershipValue"
@oninput="@(i => OnAdditionalOwnershipChange.InvokeAsync((Filter.OwnershipType, i.Value.ToString())))"
class="form-control mt-2"
placeholder="Last Name"
spellcheck="false"
autocomplete="false"/>
}
else if (Filter?.OwnershipType == OwnershipTypes.OrganizationName)
{
<InputText type="search"
id="DocTypeFilter"
@bind-Value="@Filter.OwnershipValue"
@oninput="@(i => OnOwnershipChange.InvokeAsync((Filter.OwnershipType, i.Value.ToString())))"
class="form-control"
placeholder="Organization Name"
spellcheck="false"
autocomplete="false"/>
}
else if (Filter?.OwnershipType == OwnershipTypes.MemberNumber ||
Filter?.OwnershipType == OwnershipTypes.AccountNumber ||
Filter?.OwnershipType == OwnershipTypes.OrganizationNumber ||
Filter?.OwnershipType == OwnershipTypes.PersonNumber
)
{
<InputText type="number"
id="DocTypeFilter"
@bind-Value="@Filter.OwnershipValue"
@oninput="@(i => OnOwnershipChange.InvokeAsync((Filter.OwnershipType, i.Value.ToString())))"
class="form-control"
placeholder="########"
spellcheck="false"
autocomplete="false"/>
}
</div>
</div>
@code
{
[Parameter]
public BusinessViewFilter Filter { get; set; }
[Parameter]
public EventCallback<(OwnershipTypes? type, string? value)> OnOwnershipChange { get; set; }
[Parameter]
public EventCallback<(OwnershipTypes? type, string? value)> OnAdditionalOwnershipChange { get; set; }
private string SelectedOption => Filter is { OwnershipType: {} type } ? _ownershipFilters[type] : string.Empty;
private static Dictionary<OwnershipTypes, string> _ownershipFilters = new()
{
{ OwnershipTypes.MemberNumber, "Member #" }, // CoreApi | api/core/members/{memberNumber}
{ OwnershipTypes.AccountNumber, "Account #" }, // DocumentInformation
{ OwnershipTypes.PersonName, "Person Name" }, // CoreApi | api/core/Person/GetPerson
{ OwnershipTypes.OrganizationName, "Organization Name" }, // CoreApi | api/core/Organization/{organizationName}
{ OwnershipTypes.OrganizationNumber, "Org #" }, // DocumentInformation
{ OwnershipTypes.PersonNumber, "Person #" } // DocumentInformation
};
private Task<IEnumerable<string>> FilterMemberExclusions(string search)
{
return Task.FromResult(_ownershipFilters.Where(s => s.Value.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value));
}
private void MemberExclusionTypeSelected(string ownershipType)
{
Filter.OwnershipType = ownershipType is {} type
? _ownershipFilters.FirstOrDefault(x => x.Value.Equals(type, StringComparison.OrdinalIgnoreCase)).Key
: null;
Filter.OwnershipValue = string.Empty;
Filter.AdditionalOwnershipValue = string.Empty;
OnOwnershipChange.InvokeAsync((Filter.OwnershipType, Filter.OwnershipValue));
if(Filter.OwnershipType == OwnershipTypes.PersonName) OnAdditionalOwnershipChange.InvokeAsync((Filter.OwnershipType, Filter.AdditionalOwnershipValue));
}
private void SetMaxInclusive(DateTime? createdDateMax)
{
Filter.DocumentProcessingFilter.Filter.CreatedDateMaximum = createdDateMax?.AddDays(1).AddTicks(-1);
}
}
@@ -0,0 +1,54 @@
<div class="form-row col-12">
<div class="col-6" style="padding-left: 4px;">
<label class="col-12 col-form-label">Doc Business Name:</label>
<input class="form-control" type="search" @bind="@Filter.DocumentBusinessName"/>
</div>
<div class="col-6" style="padding-left: 4px;">
<label class="col-12 col-form-label">Doc Member Name:</label>
<input class="form-control" type="search" @bind="@Filter.DocumentMemberFacingName"/>
</div>
</div>
<div class="form-row col-12">
<div class="col-12">
<label class="col-12 col-form-label">Exclude from Member View:</label>
<BlazoredTypeahead SearchMethod="@FilterMemberExclusions"
TValue="@string"
TItem="@string"
Value="@_memberExclusionType"
ValueExpression="@(() => _memberExclusionType)"
ValueChanged="@MemberExclusionTypeSelected"
EnableDropDown="true"
placeholder="Select an option...">
<SelectedTemplate>
@context
</SelectedTemplate>
<ResultTemplate>
@context
</ResultTemplate>
</BlazoredTypeahead>
</div>
</div>
@code
{
[Parameter]
public DocumentCodeDetailsFilter Filter { get; set; }
private string _memberExclusionType = string.Empty;
protected override void OnParametersSet()
{
_memberExclusionType = Filter.ExcludeFromMemberView;
}
private Task<IEnumerable<string>> FilterMemberExclusions(string search)
{
return Task.FromResult(new[] { "None", "Date Range", "All" }.Where(s => s.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)));
}
private void MemberExclusionTypeSelected(string memberExclusion)
{
Filter.ExcludeFromMemberView = _memberExclusionType = memberExclusion;
}
}
@@ -0,0 +1,58 @@
<div class="form-row col-12">
<div class="col-6" style="padding-left: 4px;">
<label class="col-12 col-form-label">Doc Ingestion Date Start:</label>
<InputDate id="CreatedDateStart"
class="form-control"
TValue="DateTime?"
@bind-Value="@Filter.CreatedDateStart"
@onblur="@ValidateForm"/>
</div>
<div class="col-6" style="padding-left: 4px;">
<label class="col-12 col-form-label">Doc Ingestion Date End:</label>
<InputDate id="CreatedDateEnd"
class="form-control"
TValue="DateTime?"
Value="@Filter.CreatedDateEnd"
ValueExpression="@(() => Filter.CreatedDateEnd)"
ValueChanged="@SetMaxInclusive"
@onblur="@ValidateForm"/>
</div>
</div>
<div class="form-row col-12">
<div class="col-6">
<label class="col-12 col-form-label">Doc Business Name:</label>
<InputText type="search" id="DocBusinessNameFilter" @bind-Value="Filter.DocBusinessName" class="form-control" placeholder="Type to filter Doc Business Name records..."
spellcheck="false" autocomplete="false"/>
</div>
<div class="col-6">
<label class="col-12 col-form-label">Doc Type:</label>
<InputText type="search" id="DocTypeFilter" @bind-Value="Filter.DocType" class="form-control" placeholder="Type to filter Doc Type records..."
spellcheck="false" autocomplete="false"/>
</div>
</div>
@if (AdditionalFilters != null && Filter != null)
{
@AdditionalFilters(Filter)
}
@code
{
[Parameter]
public Filter Filter { get; set; }
[Parameter]
public RenderFragment<Filter> AdditionalFilters { get; set; }
[Parameter]
public EventCallback Validate { get; set; }
private async void ValidateForm() => await Validate.InvokeAsync();
private void SetMaxInclusive(DateTime? createdDateEnd)
{
Filter.CreatedDateEnd = createdDateEnd?.AddDays(1).AddTicks(-1);
}
}
@@ -0,0 +1,44 @@
@inject AppConfiguration AppConfiguration;
<label class="col-12 col-form-label">Notification Status:</label>
<BlazoredTypeahead SearchMethod="@FilterDocumentProcessingInfosByStatus"
TValue="@string"
TItem="@string"
Values="@_selectedStatuses"
ValuesExpression="@(() => _selectedStatuses)"
ValuesChanged="@NotificationStatusTypeSelected"
EnableDropDown="true"
placeholder="Select an option...">
<SelectedTemplate>
@context
</SelectedTemplate>
<ResultTemplate>
@context
</ResultTemplate>
</BlazoredTypeahead>
@code {
[Parameter]
public Filter Filter { get; set; }
private static readonly IReadOnlySet<string> StatusList = Enum.GetValues<NotificationStatusTypes>()
.Select(x => x.ToString())
.ToHashSet();
private IList<string> _selectedStatuses = new List<string>();
protected override void OnParametersSet()
=> _selectedStatuses = (Filter.NotificationStatusFilters ?? Enumerable.Empty<NotificationStatusTypes>())
.Select(f => f.ToString())
.ToList();
private async Task<IEnumerable<string>> FilterDocumentProcessingInfosByStatus(string search)
=> StatusList.Where(s => s.Contains(search, StringComparison.OrdinalIgnoreCase));
private void NotificationStatusTypeSelected(IList<string> selectedStatuses)
=> Filter.NotificationStatusFilters = (_selectedStatuses = selectedStatuses)
.Select(Enum.Parse<NotificationStatusTypes>)
.ToList();
}
@@ -0,0 +1,86 @@
@inject AppConfiguration AppConfiguration;
<label class="col-12 px-3 col-form-label">Process Status:</label>
<BlazoredTypeahead SearchMethod="@FilterDocumentProcessingInfosByStatus"
TValue="@string"
TItem="@string"
Values="@_selectedStatuses"
ValuesExpression="@(() => _selectedStatuses)"
ValuesChanged="@ProcessStatusTypeSelected"
EnableDropDown="true"
placeholder="Select an option...">
<SelectedTemplate>
@context
</SelectedTemplate>
<ResultTemplate>
@context
</ResultTemplate>
</BlazoredTypeahead>
@code {
[Parameter]
public Filter Filter { get; set; }
[Parameter]
public EventCallback Validate { get; set; }
private const string Unknown = "Unknown";
private const string Delimiter = "-";
private IList<string> _selectedStatuses = new List<string>();
private async void ValidateForm() => await Validate.InvokeAsync();
protected override void OnParametersSet()
{
_selectedStatuses = Filter.StatusFilters
.Where(f => f.IsEnabled)
.Select(f => f.IsUnknown ? $"{Unknown}{Delimiter}{f.StatusType}" : f.StatusType.ToString())
.ToList();
}
private Task<IEnumerable<string>> FilterDocumentProcessingInfosByStatus(string search)
{
var statusList = Enum.GetValues<ProcessStatusTypes>().Select(x => x.ToString()).ToList();
statusList.AddRange(AddUnknownEnabledStatuses(statusList));
return Task.FromResult(statusList.Where(s => s.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)));
}
private IEnumerable<string?> AddUnknownEnabledStatuses(IEnumerable<string> statusList)
{
return AppConfiguration.UnknownEnabledStatuses
.Split(',')
.Select(status => statusList.Contains(status)
? $"{Unknown}{Delimiter}{status}"
: null).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
}
private void ProcessStatusTypeSelected(IList<string> selectedStatuses)
{
Filter.StatusFilters = (_selectedStatuses = selectedStatuses)
.Select(statusString =>
{
if (statusString.StartsWith(Unknown))
{
return new StatusFilter
{
IsEnabled = true,
IsUnknown = true,
StatusType = Enum.Parse<ProcessStatusTypes>(statusString.Split(Delimiter).Last())
};
}
return new StatusFilter
{
IsEnabled = true,
IsUnknown = false,
StatusType = Enum.Parse<ProcessStatusTypes>(statusString)
};
}
).ToList();
ValidateForm();
}
}
@@ -0,0 +1,151 @@
@inject AppConfiguration AppConfiguration;
<div class="form-row col-12">
<label class="col-12 px-3 col-form-label">
Search Criteria:&nbsp;
<i class="fa fa-info-circle" style="cursor: pointer;" data-toggle="tooltip" data-placement="top"
title="Note: The search will return only data for the specified document sources."></i>
</label>
</div>
<div class="col-6">
<BlazoredTypeahead SearchMethod="FilterDocumentProcessingInfosBySearchCriteria"
Value="@_selectedCriteria"
ValueChanged="@CriteriaChanged"
ValueExpression="@( () => _selectedCriteria )"
EnableDropDown="true"
TValue="@string"
TItem="@string"
placeholder="Select an option...">
<SelectedTemplate>
@context
</SelectedTemplate>
<ResultTemplate>
@context
</ResultTemplate>
</BlazoredTypeahead>
</div>
<div class="col-6">
@if (!string.IsNullOrWhiteSpace(_selectedCriteria))
{
<InputText id="CustomFilter"
type="text"
class="form-control col-12"
placeholder="Search Value"
@bind-Value="@_criteriaValue"
@oninput="@(e => ProcessCriteriaValue(e.Value!.ToString()))"/>
}
else
{
Filter.CustomIndexFileFiltersLikeOr = null;
}
</div>
<div class="form-row col-12">
@if (!string.IsNullOrWhiteSpace(_selectedCriteria))
{
<label class="col-12 px-3 col-form-label" style="color: #777; font-size: 85%;">
Returned Types: @string.Join(", ", _criteria[_selectedCriteria]._docTypes.Select(x => Enum.GetName(typeof(IngestorTypes), x)))
</label>
}
</div>
<span class="col-12 col-form-label" style="display:inline-block;">
<ValidationMessage For="@(() => Filter.CustomIndexFileFiltersLikeOr)"/>
</span>
@code {
[Parameter]
public Filter Filter { get; set; }
[Parameter]
public EventCallback Validate { get; set; }
private string _selectedCriteria = string.Empty;
private string _criteriaValue = string.Empty;
private Dictionary<string, (IEnumerable<IngestorTypes> _docTypes, IEnumerable<string> _filters)> _criteria = new()
{
{"Account Number",
(
new List<IngestorTypes> { IngestorTypes.XdiDnaNotice,
IngestorTypes.OsgDnaNoticesDocument,
IngestorTypes.FosTaxStatement,
IngestorTypes.FosLoanStatement,
IngestorTypes.FosDnaDocument },
new List<string> {"AccountNumber"}
)
},
{"Member Number Or User Id",
(
new List<IngestorTypes> { IngestorTypes.OsgDnaNoticesDocument },
new List<string> {"MemberNumberOrUserId"}
)
},
{"Entity Name",
(
new List<IngestorTypes> { IngestorTypes.XdiDnaNotice,
IngestorTypes.XdiActionNotice,
IngestorTypes.FosTaxStatement,
IngestorTypes.FosLoanStatement,
IngestorTypes.FosDnaDocument },
new List<string> {"EntityName", "FullName"}
)
}
};
private async void ValidateForm() => await Validate.InvokeAsync();
protected override void OnParametersSet()
{
if (Filter.CustomIndexFileFiltersLikeOr is null or { Count: 0 })
{
_selectedCriteria = _criteriaValue = string.Empty;
}
else if (string.IsNullOrWhiteSpace(_selectedCriteria))
{
Filter.CustomIndexFileFiltersLikeOr = null;
_criteriaValue = string.Empty;
}
}
private void CriteriaChanged(string result)
{
_selectedCriteria = result;
_criteriaValue = string.Empty;
Filter.CustomIndexFileFiltersLikeOr = AssignFilters();
ValidateForm();
}
private Task<IEnumerable<string>> FilterDocumentProcessingInfosBySearchCriteria(string search)
{
return Task.FromResult(_criteria.Keys.ToList().Where(s => s.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)));
}
private void ProcessCriteriaValue(string selectedValue)
{
if (!string.IsNullOrWhiteSpace(_selectedCriteria))
{
Filter.CustomIndexFileFiltersLikeOr = AssignFilters(selectedValue);
}
ValidateForm();
}
private Dictionary<string,string>? AssignFilters(string value = "")
{
if (string.IsNullOrWhiteSpace(_selectedCriteria))
{
return null;
}
var dictionary = new Dictionary<string, string>();
foreach (var filter in _criteria[_selectedCriteria]._filters)
{
dictionary[filter] = value;
}
return dictionary;
}
}
@@ -0,0 +1,10 @@
<div class="grid-subheader-row-container processing-header">
<div class="col">
<h7><b>@ChildContent</b></h7>
</div>
</div>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@@ -0,0 +1,54 @@
@using System.ComponentModel.DataAnnotations
<div class="form-row col-12">
<div class="col-6">
<label class="col-12 col-form-label">Ingestor Run Id:</label>
<InputText type="search"
id="IngestorRunInfoIdfilter"
class="form-control"
@bind-Value="_ingestorRunInfoIdString"
@oninput="IngestorRunInfoIdChanged"
placeholder="Type to filter records by Ingestor Run Id..."
spellcheck="false" autocomplete="false"/>
@if (!_ingestorInfoIdIsValid)
{
<span style="color: red;" class="pl-3">Ingestor Run Id must be a guid</span>
}
</div>
<div class="col-6"></div>
</div>
@code
{
[Parameter]
public Filter Filter { get; set; }
private string? _ingestorRunInfoIdString = null;
private bool _ingestorInfoIdIsValid = true;
protected override void OnParametersSet()
{
if (Filter.IngestorRunInfoId == null) _ingestorRunInfoIdString = null;
}
private void IngestorRunInfoIdChanged(ChangeEventArgs changeEventArgs)
{
if (string.IsNullOrWhiteSpace(changeEventArgs.Value?.ToString()))
{
Filter.IngestorRunInfoId = null;
_ingestorRunInfoIdString = null;
}
else if (Guid.TryParse(changeEventArgs.Value?.ToString(), out var guid))
{
Filter.IngestorRunInfoId = guid;
_ingestorRunInfoIdString = Filter.IngestorRunInfoId.ToString();
}
else
{
Filter.IngestorRunInfoId = Guid.Empty;
_ingestorRunInfoIdString = changeEventArgs.Value?.ToString();
}
_ingestorInfoIdIsValid = Filter.IngestorRunInfoId != Guid.Empty;
}
}
@@ -0,0 +1,20 @@
<div class="modal fade show d-block d-flex justify-content-center align-items-center" tabindex="-1" role="dialog">
<div class="modal-backdrop fade show"></div>
<div class="modal-dialog loading-lighthouse">
<img src="images/lighthouse.gif" alt="LCU Lighthouse" class="loading-lighthouse">
</div>
</div>
@code {
public static IModalReference Show(IModalService modal)
{
var options = new ModalOptions
{
UseCustomLayout = true
};
return modal.Show<LoadingLighthouse>(string.Empty, options);
}
}
@@ -0,0 +1,9 @@
div.loading-lighthouse {
z-index: 1050;
text-align: center
}
img.loading-lighthouse {
width: 200px;
height: 200px;
}
@@ -0,0 +1,48 @@
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Html
@using Microsoft.AspNetCore.Mvc.ViewFeatures
@inherits ConfirmBase
@if (ShowConfirmation)
{
<div class="modal fade show d-flex" id="exampleModal" tabindex="-1" role="dialog">
<div class="modal-dialog align-self-center modal-lg " role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">@ConfirmationTitle</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"
@onclick="() => OnConfirmationChange(false)">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
@if (ConfirmationMessages.Any())
{
foreach (var message in ConfirmationMessages)
{
<p class="mb-2">
@((MarkupString)message)
</p>
}
}
else
{
@ConfirmationMessage
}
</div>
@if (!string.IsNullOrWhiteSpace(WarningMessage))
{
<div class="modal-body confirm-warning">
@WarningMessage
</div>
}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"
@onclick="() => OnConfirmationChange(false)">@CancelButtonText</button>
<button type="button" class="btn btn-danger"
@onclick="() => OnConfirmationChange(true)">@ConfirmButtonText</button>
</div>
</div>
</div>
</div>
}
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Components;
namespace LCU.WebApps.Internal.DocumentManager.Shared.Components.Modals;
public class ConfirmBase : ComponentBase
{
protected bool ShowConfirmation { get; set; }
[Parameter]
public string ConfirmationTitle { get; set; } = "Confirm";
[Parameter]
public string ConfirmationMessage { get; set; } = "Are you sure?";
[Parameter]
public List<string> ConfirmationMessages { get; set; } = new();
[Parameter]
public string WarningMessage { get; set; } = string.Empty;
[Parameter]
public string CancelButtonText { get; set; } = "Cancel";
[Parameter]
public string ConfirmButtonText { get; set; } = "Confirm";
public void Show()
{
ShowConfirmation = true;
StateHasChanged();
}
[Parameter]
public EventCallback<bool> ConfirmationChanged { get; set; }
protected async Task OnConfirmationChange(bool value)
{
ShowConfirmation = false;
await ConfirmationChanged.InvokeAsync(value);
}
}
@@ -0,0 +1,45 @@
@inherits ModalBase
@if (Visible)
{
<div class="modal fade show d-flex" id="exampleModal" tabindex="-1" role="dialog">
<div class="modal-dialog align-self-center" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">@Title</h5>
<button type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@onclick="@(() => Close(false))">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
@ChildContent
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-secondary"
data-dismiss="modal"
@onclick="@(() => Close(false))">
Cancel
</button>
<button type="button"
class="btn btn-danger"
disabled="@(!ConfirmAllowed)"
@onclick="@(() => Close(true))">
Confirm
</button>
</div>
</div>
</div>
</div>
}
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Components;
namespace LCU.WebApps.Internal.DocumentManager.Shared.Components.Modals;
public class ModalBase : ComponentBase
{
[Parameter]
public string Title { get; set; }
[Parameter]
public EventCallback<bool> OnClose { get; set; }
[Parameter]
public bool ConfirmAllowed { get; set; } = true;
protected bool Visible { get; set; }
public void Show()
{
Visible = true;
StateHasChanged();
}
protected async Task Close(bool confirmed)
{
Visible = false;
await OnClose.InvokeAsync(confirmed);
}
}
@@ -0,0 +1,76 @@
@inject AppConfiguration AppConfiguration;
@inject IToastService ToastService;
@inject IDocumentReprocessingService QueueDocumentReprocessingServices;
@inject IDocumentProcessingInfoService DocumentProcessingInfoService;
<div>
<Confirm @ref="ReprocessCriticalFailuresConfirmation" ConfirmationChanged="ConfirmReprocessCriticalFailures_Click"
ConfirmationMessage=@("Are you sure you want to reprocess all CriticalFailures?")
WarningMessage=@_limitMessage
ConfirmationTitle=@("Reprocess Critical Failures")>
</Confirm>
<button id="reprocessAllCF"
disabled="@ReprocessAllCfDisabled"
type="button"
class="btn btn-warning reprocessBtn @(ReprocessAllCfDisabled ? "btnLoading" : string.Empty)"
title="ReprocessCriticalFailures"
@onclick="ReprocessCriticalFailures_Click">
Reprocess Critical Failures
</button>
</div>
@code
{
private ConfirmBase ReprocessCriticalFailuresConfirmation { get; set; }
private bool ReprocessAllCfDisabled { get; set; } = false;
private const int FilterLimit = 1000;
private string _limitMessage = string.Empty;
private async void ReprocessCriticalFailures_Click()
{
var count = await CountCriticalFailures();
_limitMessage = count > FilterLimit
? $"Reprocess Critical Failures is limited to {FilterLimit} documents. " +
$"Remaining {count - FilterLimit} documents will not be reprocessed."
: string.Empty;
StateHasChanged();
ReprocessCriticalFailuresConfirmation.Show();
}
private async Task ConfirmReprocessCriticalFailures_Click(bool deleteConfirmed)
{
if (deleteConfirmed)
{
ReprocessAllCfDisabled = true;
await ReprocessCriticalFailures();
ReprocessAllCfDisabled = false;
}
}
private async Task ReprocessCriticalFailures()
{
try
{
ToastService.ShowInfo("Queueing CriticalFailure documents for reprocessing...");
await QueueDocumentReprocessingServices.QueueDocumentReprocessingAllCriticalFailures();
ToastService.ShowInfo("CriticalFailure documents are set for re-processing.");
}
catch
{
}
}
private async Task<int> CountCriticalFailures()
{
var allCriticalFailureFilter = new Filter()
{
Page = 1,
Limit = 1,
StatusFilters = new() { new() { IsEnabled = true, IsUnknown = false, StatusType = ProcessStatusTypes.CriticalFailure}}
};
return (await DocumentProcessingInfoService.GetFilteredDocumentProcessingInfos(allCriticalFailureFilter)).TotalItems;
}
}
@@ -0,0 +1,77 @@
@inject AppConfiguration AppConfiguration;
@inject IToastService ToastService;
@inject IDocumentReprocessingService DocumentReprocessingService;
@inject IDocumentProcessingInfoService DocumentProcessingInfoService;
<div>
<Confirm @ref="ReprocessConfirmation" ConfirmationChanged="OnConfirm"
ConfirmationMessage=@($"Are you sure you want to reprocess all {StatusName}s?")
WarningMessage=@_limitMessage
ConfirmationTitle=@("Reprocess Notification Data Failures")>
</Confirm>
<button
disabled="@Disabled"
type="button"
class="btn btn-warning reprocessBtn @(Disabled ? "btnLoading" : string.Empty)"
title="ReprocessNotificationDataFailures"
@onclick="OnClick">
Reprocess Notification Data Failures
</button>
</div>
@code
{
private const string StatusName = nameof(ProcessStatusTypes.NotificationDataFailure);
private ConfirmBase ReprocessConfirmation { get; set; } = null!;
private bool Disabled { get; set; } = false;
private const int FilterLimit = 1000;
private string _limitMessage = string.Empty;
private async void OnClick()
{
var count = await CountNotificationFailures();
_limitMessage = count > FilterLimit
? $"Reprocess Notification Data Failures is limited to {FilterLimit} records. " +
$"Remaining {count - FilterLimit} records will not be reprocessed."
: string.Empty;
StateHasChanged();
ReprocessConfirmation.Show();
}
private async Task OnConfirm(bool confirmed)
{
if (confirmed)
{
Disabled = true;
await Reprocess();
Disabled = false;
}
}
private async Task Reprocess()
{
try
{
ToastService.ShowInfo($"Queueing {StatusName} documents for reprocessing...");
await DocumentReprocessingService.ReprocessAllNotificationDataFailures();
ToastService.ShowInfo($"{StatusName} documents are set for reprocessing.");
}
catch
{
}
}
private async Task<int> CountNotificationFailures()
{
var allNotificationFailureFilter = new Filter()
{
Page = 1,
Limit = 1,
StatusFilters = new() { new() { IsEnabled = true, IsUnknown = false, StatusType = ProcessStatusTypes.NotificationDataFailure}}
};
return (await DocumentProcessingInfoService.GetFilteredDocumentProcessingInfos(allNotificationFailureFilter)).TotalItems;
}
}
@@ -0,0 +1,91 @@
@using Serilog
@inject AppConfiguration AppConfiguration;
@inject IToastService ToastService;
@inject IDocumentProcessingInfoService DocumentProcessingInfoService;
@inject IJSTooltips JSTooltips;
<div>
<Confirm @ref="SetEntityToUnknownConfirmation" ConfirmationChanged="ConfirmSetEntitiesToUnknown_Click"
ConfirmationMessage=@($"Are you sure you want to set Entity to Unknown for {ViewModel.TotalItems} documents?")
WarningMessage=@_limitMessage
ConfirmationTitle=@("Set Entity to Unknown")>
</Confirm>
<button id=""
disabled="@(SetEntityToUnknownDisabled())"
type="button"
class="btn btn-warning reprocessBtn @(_loading ? "btnLoading" : string.Empty)"
title="Set Entities to Unknown"
@onclick="SetEntityToUnknown_Click">
Unknown @if (!SetEntityToUnknownDisabled())
{
@("(" + ViewModel.TotalItems + " documents)")
}
<i class="fa fa-info-circle @(SetEntityToUnknownDisabled() ? "" : "collapse")" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="Enabled when filter has values for Doc Ingestion Dates (Start and End), Doc Business Name, and Process Status is set to ONLY DataMappingFailure."></i>
</button>
</div>
@code
{
[Parameter]
public ITablePagingViewModel<DocumentProcessingInfo, Filter> ViewModel { get; set; }
private ConfirmBase? SetEntityToUnknownConfirmation { get; set; }
private const int FilterLimit = 1000;
private string _limitMessage = string.Empty;
private bool _loading = false;
private bool SetEntityToUnknownDisabled()
{
return string.IsNullOrWhiteSpace(ViewModel.Filter.DocBusinessName) ||
ViewModel.Filter.CreatedDateStart == null ||
ViewModel.Filter.CreatedDateEnd == null ||
ViewModel.Filter.StatusFilters.Count() != 1 ||
ViewModel.Filter.StatusFilters.Count(x => x is {StatusType: ProcessStatusTypes.DataMappingFailure, IsUnknown: false, IsEnabled: true }) != 1 ||
ViewModel.TotalItems == 0 ||
_loading;
}
private bool TotalItemsExceedsLimit() => ViewModel.TotalItems > FilterLimit;
protected override Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender) JSTooltips.Enable();
return base.OnAfterRenderAsync(firstRender);
}
private void SetEntityToUnknown_Click()
{
_limitMessage = TotalItemsExceedsLimit()
? $"Setting Entity to Unknown is limited to {FilterLimit} documents. " +
$"Remaining {ViewModel.TotalItems - FilterLimit} documents will not be processed."
: string.Empty;
StateHasChanged();
SetEntityToUnknownConfirmation?.Show();
}
private async Task ConfirmSetEntitiesToUnknown_Click(bool setEntityConfirmed)
{
if (setEntityConfirmed)
{
await SetEntityToUnknown();
}
}
private async Task SetEntityToUnknown()
{
var totalItems = ViewModel.TotalItems < FilterLimit ? ViewModel.TotalItems : FilterLimit;
ToastService.ShowInfo($"Setting Entity to Unknown for {totalItems} documents.");
try
{
_loading = true;
await DocumentProcessingInfoService.SetDocumentsToEntityUnknownByFilter(ViewModel.Filter, totalItems);
_loading = false;
ToastService.ShowInfo($"Entities have been set to Unknown for {totalItems} documents.");
}
catch { }
await ViewModel.Refresh();
}
}
@@ -0,0 +1,152 @@
@using LCU.Library.Internal.FileOperationsModels.Shared
@inject IJSTooltips JSTooltips;
@inject IDocumentProcessingInfoService DocumentProcessingInfoService;
@inject IModalService _modal;
<div>
<Modal @ref="ChangeNotificationModal"
OnClose="@ShowConfirmation"
ConfirmAllowed="@ConfirmAllowed"
Title="Set Notification Status">
<div>
<label class="col-form-label text-nowrap required" for="select-status">Notification Status:</label>
<select id="select-status" class="form-control" required @bind="@_pendingChange.Status">
<option/>
@foreach (var value in Enum.GetValues<NotificationStatusTypes>())
{
<option value="@value">@value.ToString()</option>
}
</select>
</div>
<div>
<label class="col-form-label text-nowrap required" for="change-reason">Change Reason:</label>
<input id="change-reason" class="form-control" type="text" required value="@_pendingChange.ChangeReason" @oninput="@(change => _pendingChange.ChangeReason = change.Value?.ToString() ?? string.Empty)"/>
</div>
</Modal>
<Confirm @ref="Confirmation"
ConfirmationMessage="@($"Update {_potentialUpdates} to {_pendingChange.Status} because {_pendingChange.ChangeReason}?")"
ConfirmationChanged="@OnConfirm"/>
<button disabled="@(Disabled || _potentialUpdates == 0)"
type="button"
class="btn btn-warning reprocessBtn @(_updating ? "btnLoading" : string.Empty)"
title="Batch Update Notification Status"
@onclick="@BatchSetNotificationStatus_Click">
Update Notification Status
@if (!DisabledByFilter)
{
@($"({_potentialUpdates} documents)")
}
<i class="fa fa-info-circle @(DisabledByFilter ? "" : "collapse")" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="Enabled when the filter has a valid Document Ingestion ID."></i>
</button>
</div>
@code {
[Parameter]
public Filter Filter { get; set; }
[Parameter]
public EventCallback<IModalReference> RefreshData { get; set; }
private bool Disabled => _updating || DisabledByFilter;
private bool DisabledByFilter => Filter?.IngestorRunInfoId is null || Filter.IngestorRunInfoId == Guid.Empty;
private bool ConfirmAllowed => _pendingChange.Status != null && !string.IsNullOrWhiteSpace(_pendingChange.ChangeReason);
private Modal ChangeNotificationModal { get; set; } = null!;
private Confirm Confirmation { get; set; } = null!;
private IModalReference openModal;
private Filter _filter = new();
private int? _potentialUpdates;
private bool _updating = false;
private NotificationStatusChange _pendingChange = DefaultChangeFactory();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await JSTooltips.Enable();
if (!Disabled && !AreEqual(_filter, Filter))
{
_filter = Filter.DeepClone();
if (_potentialUpdates != (_potentialUpdates = await CountStatusUpdates()))
{
StateHasChanged();
}
}
}
private async Task<int> CountStatusUpdates()
{
var allNotificationStatusFilter = Filter.DeepClone();
allNotificationStatusFilter.Page = 1;
allNotificationStatusFilter.Limit = 1;
if (allNotificationStatusFilter.NotificationStatusFilters is null or { Count: 0 })
{
allNotificationStatusFilter.NotificationStatusFilters = Enum.GetValues<NotificationStatusTypes>().ToList();
}
return (await DocumentProcessingInfoService.GetFilteredDocumentProcessingInfos(allNotificationStatusFilter)).TotalItems;
}
private static NotificationStatusChange DefaultChangeFactory()
=> new()
{
Status = null,
ChangeReason = string.Empty
};
private void BatchSetNotificationStatus_Click()
{
ChangeNotificationModal.Show();
}
private void ShowConfirmation(bool confirmed)
{
if (confirmed)
{
Confirmation.Show();
}
else
{
_pendingChange = DefaultChangeFactory();
}
}
private async Task OnConfirm(bool confirmed)
{
if (confirmed)
{
try
{
openModal = LoadingLighthouse.Show(_modal);
_updating = true;
await DocumentProcessingInfoService.BulkSetDocumentNotificationStatus(Filter, _pendingChange.Status.Value, _pendingChange.ChangeReason);
_updating = false;
await RefreshData.InvokeAsync(openModal);
}
catch
{ }
_pendingChange = DefaultChangeFactory();
}
else
{
ChangeNotificationModal.Show();
}
}
private record NotificationStatusChange
{
public NotificationStatusTypes? Status { get; set; }
public string ChangeReason { get; set; }
};
private bool AreEqual<T>(T object1, T object2)
{
var obj1 = JsonConvert.SerializeObject(object1);
var obj2 = JsonConvert.SerializeObject(object2);
var compare = string.Compare(obj1, obj2, StringComparison.OrdinalIgnoreCase);
return compare == 0;
}
}
@@ -0,0 +1,35 @@
@ChildContent((Working, ExecuteAction))
@code {
[Parameter]
public Func<Task<bool>> Action { get; set; }
[Parameter]
public Action? OnSuccess { get; set; }
[Parameter]
public RenderFragment<(bool working, Func<Task> Action)> ChildContent { get; set; }
private readonly SemaphoreSlim _otherActions = new (1);
private bool Working => _otherActions.CurrentCount == 0;
private async Task ExecuteAction()
{
_otherActions.WaitAsync();
StateHasChanged();
try
{
var succeeded = await Action();
if (succeeded && OnSuccess is {} success) success();
}
finally
{
_otherActions.Release();
StateHasChanged();
}
}
}
@@ -0,0 +1,15 @@
<div class="row">
<div class="col-md-5 ml-3">
<b>@Label:</b>
</div>
<div class="col-md-7">
@AttributeValue
</div>
</div>
@code {
[Parameter]
public string Label { get; set; }
[Parameter]
public string AttributeValue { get; set; }
}
@@ -0,0 +1,69 @@
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@using Serilog
@inject IJSRuntime JSRuntime;
@inject IToastService ToastService;
@inject FileOperationsApiConfiguration _fileOperationsApiConfiguration;
@inject AuthenticationHelper Auth;
<RowSubHeader>Document Attributes</RowSubHeader>
<div class="pt-2 pl-3">
@switch (DocumentProcessingInfo.IngestorRunInfo.IngestorType)
{
case IngestorTypes.FosTaxStatement:
<FosTaxStatementComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
case IngestorTypes.FosDnaDocument:
<FosDnaDocumentComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
case IngestorTypes.XdiDnaNotice:
<XdiDnaNoticeComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
case IngestorTypes.FosLoanStatement:
<FosLoanStatementDocumentComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
case IngestorTypes.OsgDnaNoticesDocument:
<OsgDnaNoticesDocumentComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
case IngestorTypes.OsgHistoricalDocument:
<OsgHistoricalDocumentComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
case IngestorTypes.OsgWeilandStatement:
<OsgWeilandStatementComponent DocumentProcessingInfo=DocumentProcessingInfo />
break;
}
<div class="row justify-content-center mt-4 mb-2">
<button type="button" class="btn btn-primary" @onclick="(() => OpenDocument())" title="OpenDocument" disabled="@Disabled">
Open Document
</button>
</div>
</div>
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private bool Disabled => !Auth.PolicySatisfied(PolicyConstants.ReprocessingTableOpenDocument);
private async Task OpenDocument()
{
try
{
var filePath = DocumentProcessingInfo.IngestorRunInfo.IndexFilePath + DocumentProcessingInfo.DocumentFileName;
var finalFilePath = filePath.Replace("\\", "%5C");
var url = $"{_fileOperationsApiConfiguration.Url}" +
$"{_fileOperationsApiConfiguration.GetPdfContentEndpoint}" +
$"/{finalFilePath}";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
catch (Exception exception)
{
var message = "Error opening document.";
Log.Error(message, exception);
ToastService.ShowError(message);
}
}
}
@@ -0,0 +1,24 @@
<AttributeFieldComponent Label=@AccountLabel AttributeValue=@Account />
<div class="row">
<div class="col-md-6 ml-3">
<b>@MemberNumberOrUserIdLabel:</b>
</div>
<div class="col-md-7">
@MemberNumberOrUserId
</div>
</div>
@code {
[Parameter]
public string Account { get; set; }
[Parameter]
public string MemberNumberOrUserId { get; set; }
private string AccountLabel = "Account Number";
private string MemberNumberOrUserIdLabel = "Member Number Or User Id";
protected override void OnParametersSet()
{
Account = string.IsNullOrWhiteSpace(Account) ? "N/A" : Account;
}
}
@@ -0,0 +1,16 @@
<AttributeFieldComponent Label=@AccountLabel AttributeValue=@Account />
<AttributeFieldComponent Label=@NameLabel AttributeValue=@Name />
@code {
[Parameter]
public string Account { get; set; }
[Parameter]
public string Name { get; set; }
private string AccountLabel = "Account Number";
private string NameLabel = "Entity Name";
protected override void OnParametersSet()
{
Account = string.IsNullOrWhiteSpace(Account) ? "N/A" : Account;
}
}
@@ -0,0 +1,19 @@
<AttributeFieldComponent Label=@NameLabel AttributeValue=@Name />
<div class="row">
<div class="col-md-6 ml-3">
<b>@MemberNumberOrUserIdLabel:</b>
</div>
<div class="col-md-7">
@MemberNumberOrUserId
</div>
</div>
@code {
[Parameter]
public string Name { get; set; }
[Parameter]
public string MemberNumberOrUserId { get; set; }
private string NameLabel = "Entity Name";
private string MemberNumberOrUserIdLabel = "Member Number Or User Id";
}
@@ -0,0 +1,11 @@
<AttributeFieldComponent Label=@TinLabel AttributeValue=@Tin />
<AttributeFieldComponent Label=@NameLabel AttributeValue=@Name />
@code {
[Parameter]
public string Tin { get; set; }
[Parameter]
public string Name { get; set; }
private string TinLabel = "Entity Tin";
private string NameLabel = "Entity Name";
}
@@ -0,0 +1,12 @@
<EntityAccountAndNameComponent [email protected] [email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private FosDnaDocument DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<FosDnaDocument>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,12 @@
<EntityAccountAndNameComponent [email protected] [email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private FosLoanStatement DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<FosLoanStatement>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,12 @@
<EntityTinAndNameComponent [email protected] [email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private FosTaxStatementDocument DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<FosTaxStatementDocument>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,12 @@
<EntityAccountAndMemberNumberOrUserIdComponent [email protected] [email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private OsgDnaNoticesDocument DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<OsgDnaNoticesDocument>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,14 @@
<EntityNameAndMemberNumberOrUserIdComponent
[email protected]
[email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private OsgHistoricalDocument DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<OsgHistoricalDocument>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,12 @@
<EntityAccountAndNameComponent [email protected] [email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private OsgWeilandStatement DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<OsgWeilandStatement>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,12 @@
<EntityAccountAndNameComponent [email protected] [email protected] />
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
private XdiDnaNoticesDocument DocumentValuePayload { get; set; }
protected override void OnParametersSet()
{
DocumentValuePayload = JsonConvert.DeserializeObject<XdiDnaNoticesDocument>(DocumentProcessingInfo.DocumentValuePayload);
}
}
@@ -0,0 +1,13 @@
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView.DocumentProcessingRowBodies;
namespace LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
public class BusinessReprocessingRowBody : DocumentProcessingRowBody
{
protected override Type BodyType => DocumentProcessingInfo.StatusType switch
{
ProcessStatusTypes.DataMappingFailure => typeof(BusinessDataMappingFailure),
_ => base.BodyType
};
}
@@ -0,0 +1,87 @@
@using LCU.Library.Internal.DocumentModels.Responses
@using JsonSerializer = System.Text.Json.JsonSerializer
@inject IJSDate JSDate;
<CollapsibleComponent ButtonClass="btn btn-link"
HeaderClass="row grid-header-row grid-subheader-row-container processing-header"
ButtonContainerClass="grid-button-container"
IsCollapsed="false">
<HeaderContent>
<RowSubHeader>Audit Log</RowSubHeader>
</HeaderContent>
<BodyContent>
<div class="log-message-container overflow-auto">
@if (AuditTrail is not null && AuditTrail.Any())
{
foreach (var log in AuditTrail)
{
<div class="d-flex border-bottom">
<div class="col-3 col-with-right-boarder d-flex flex-column">
<span>
Audit Timestamp
</span>
<span>
@_localTimes?[log]
</span>
</div>
<div class="col-2 col-with-right-boarder d-flex flex-column">
<span>
User
</span>
<span>
@log.UserId
</span>
</div>
<div class="col col-with-right-boarder d-flex flex-column">
<span>
Audit Message
</span>
<span>
@FormatLogMessage(log)
</span>
</div>
</div>
}
}
else
{
<div class="d-flex">
<div class="col-12 col-with-right-boarder grid-subheader-row-container" style="border-top: 1px solid rgba(0, 0, 0, .125);">
<span>
No Audit Log information found...
</span>
</div>
</div>
}
</div>
</BodyContent>
</CollapsibleComponent>
@code
{
[Parameter]
public IEnumerable<DocumentAuditResponse> AuditTrail { get; set; }
private IDictionary<DocumentAuditResponse, string> _localTimes;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if(AuditTrail != null) {
_localTimes = (await Task.WhenAll(AuditTrail.Select(async a => (response: a, time: await JSDate.AsClientLocalTime(a.Date)))))
.ToDictionary(t => t.response, t => t.time);
}
StateHasChanged();
}
}
private static string FormatLogMessage(DocumentAuditResponse auditLog) => auditLog switch
{
DocumentActionAuditResponse log => log.ActionDescription,
DocumentFieldAuditResponse log => $"{log.Field} was changed from " +
JsonSerializer.Serialize(log.PreviousValue != null ? log.PreviousValue : "null") +
$" to {JsonSerializer.Serialize(log.CurrentValue != null ? log.CurrentValue : "null")}",
_ => throw new ArgumentOutOfRangeException(nameof(auditLog))
};
}
@@ -0,0 +1,51 @@
@inject IJSDate JSDate;
<div class="col-3 col-with-right-boarder">
<div>
Created Date
</div>
<div>
<h6>@Document.CreatedDate</h6>
</div>
</div>
<div class="col-2 col-with-right-boarder">
<div>
Document Type
</div>
<div>
<h6>@Document.DocumentType</h6>
</div>
</div>
<div class="col-3 col-with-right-boarder">
<div>
Document Business Name
</div>
<div>
<h6>@Document.DocumentBusinessName</h6>
</div>
</div>
<div class="col-3 col-with-right-boarder">
<div>
Entity Name
</div>
<div>
<h6>@Document.EntityName</h6>
</div>
</div>
@code {
[Parameter]
public DocumentInformation Document { get; set; }
private string _localCreatedDate = string.Empty;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_localCreatedDate = await JSDate.AsClientLocalTime(Document.CreatedDate);
}
}
}
@@ -0,0 +1,58 @@
using LCU.Library.Internal.DocumentModels.Responses;
using System.Collections.ObjectModel;
namespace LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
public record BusinessViewDocumentInformation
(
DocumentInformation DocumentInformation,
IEnumerable<DocumentAuditResponse> Logs
);
public record DocumentInformation(
Guid DocumentId,
Guid? TraceId,
string Source,
string DataSource,
DateTime CreatedDate,
DateTime? UploadDate,
string Name,
string DocumentType,
string DocumentCode,
string DocumentBusinessName,
string DocumentMemberFacingName,
int OnlineBankingRetentionMonths,
int InternalRetentionMonths,
string EntityName,
Dictionary<string, Dictionary<string, string>> AccountOwners,
ObservableCollection<DocumentAccess> DocumentAccess,
ObservableCollection<string> PersonNumbers,
ObservableCollection<string> OrganizationNumbers,
long? AccountNumber,
bool TypeDisabled,
bool? ScanStatus,
Func<Task> Download
)
{
//assume that documents that were uploaded 2 hours or more ago have been scanned
//"safe enough" given that
// - no real documents have set off Azure Defender
// - test malicious documents took an average of 1h:45m to be scanned
// - Azure Defender isn't integrated (yet) for document scan completion observation
public bool Scanned => UploadDate.Value.AddHours(2) <= DateTime.UtcNow;
};
public enum AccessType
{
PrimaryOwner,
Owner,
Additional,
Removed
}
public record DocumentAccess(
AccessType AccessType,
bool HasAccess,
string EntityName,
string EntityId
);
@@ -0,0 +1,37 @@
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
<div class="d-flex">
<div class="col-3 col-with-right-boarder p-0">
<DocumentAttributes
CustomerFacingName="@Document.DocumentMemberFacingName"
AccountNumber="@Document.AccountNumber?.ToString()"
EntityName="@Document.EntityName"
VisibleInMemberView="@(!Document.TypeDisabled)"
DownloadDocument="@Document.Download"
CreatedDate="@Document.CreatedDate"
OnlineBankingRetentionMonths="@Document.OnlineBankingRetentionMonths"
InternalRetentionMonths="@Document.InternalRetentionMonths"
DocumentId="@Document.DocumentId.ToString()"
ScanStatus="@Document.ScanStatus"
Scanned="@Document.Scanned"/>
</div>
<div class="col-9 p-0">
<div class="col-12 p-0">
<DocumentAuthorizationsContainer Document="@Document"/>
</div>
<div class="col-12 p-0">
<AuthorizeView Policy="@PolicyConstants.DocumentLookupEditVisibility">
<DocumentVisibilityComponent Document="@Document" DocumentAuthorization="@ViewModel"/>
</AuthorizeView>
</div>
</div>
</div>
@code {
[Parameter]
public DocumentInformation Document { get; set; }
[Parameter]
public IBusinessViewViewModel ViewModel { get; set; }
}
@@ -0,0 +1,23 @@
using DocumentTypeRetriever;
using LCU.WebApps.Internal.DocumentManager.Enums;
namespace LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
public record BusinessViewFilter
{
public OwnershipTypes? OwnershipType { get; set; } = null;
public string OwnershipValue { get; set; } = string.Empty;
public string AdditionalOwnershipValue { get; set; } = string.Empty;
public DocumentInformationFilteredSearch DocumentProcessingFilter { get; set; } = new()
{
Filter = new DocumentInformationFilter(),
SortDefinition = new List<SortableField>
{
new()
{
Field = "CreatedDate",
IsDescending = true
}
}
};
}
@@ -0,0 +1,908 @@
using System.Collections.ObjectModel;
using Blazored.Modal.Services;
using Blazored.Toast.Services;
using DocumentTypeRetriever;
using LCU.Library.DocumentType.Enums;
using LCU.Library.DocumentType.Extensions;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Accounts;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Organizations;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Persons;
using LCU.Library.Internal.DocumentIngestorModels.Enums;
using LCU.WebApps.Internal.DocumentManager.Enums;
using LCU.WebApps.Internal.DocumentManager.Models;
using LCU.WebApps.Internal.DocumentManager.Configurations;
using LCU.WebApps.Internal.DocumentManager.Services;
using LCU.WebApps.Internal.DocumentManager.Shared.Components;
using LCU.WebApps.Internal.DocumentManager.Shared.JSInterop;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Serilog;
using DocumentAction = LCU.Library.Internal.DocumentModels.Requests.DocumentAction;
using DocumentAuditResponse = LCU.Library.Internal.DocumentModels.Responses.DocumentAuditResponse;
using DocumentMetadataResponse = LCU.Library.DocumentType.Models.Responses.External.DocumentMetadataResponse;
namespace LCU.WebApps.Internal.DocumentManager.Shared.DocumentGridView;
public interface IBusinessViewViewModel : ITablePagingViewModel<BusinessViewDocumentInformation, BusinessViewFilter>, IDocumentAuthorization
{
event EventHandler DataLoadError;
void FilterChanged(OwnershipTypes? ownership, string field, string value);
}
public class BusinessViewViewModel : IBusinessViewViewModel
{
public bool Loaded { get; private set; } = false;
public BusinessViewFilter Filter { get; } = new();
public bool FilterIsValid { get; set; } = true;
public event EventHandler? OnFilterValidityChanged = delegate { };
public void FilterChanged(OwnershipTypes? ownership, string field, string value)
{
FilterIsValid = ownership switch
{
null => true,
OwnershipTypes.AccountNumber or OwnershipTypes.MemberNumber or OwnershipTypes.OrganizationNumber or OwnershipTypes.PersonNumber
=> float.TryParse(value, out _),
OwnershipTypes.PersonName when field is nameof(Filter.OwnershipType)
=> !(string.IsNullOrWhiteSpace(value) && string.IsNullOrWhiteSpace(Filter.AdditionalOwnershipValue)),
OwnershipTypes.PersonName when field is nameof(Filter.AdditionalOwnershipValue)
=> !(string.IsNullOrWhiteSpace(Filter.OwnershipValue) && string.IsNullOrWhiteSpace(value)),
OwnershipTypes.OrganizationName
=> !string.IsNullOrWhiteSpace(value),
_ => false
};
}
public ObservableCollection<BusinessViewDocumentInformation> VisibleItems { get; } = new();
public event EventHandler DataChanged = delegate { };
public event EventHandler DataLoadError = delegate { };
public int ItemsPerPage { get; private set; } = 10;
public IReadOnlyList<int> PossibleItemsPerPage { get; } = new[] { 10, 25, 50 };
public int CurrentPage { get; private set; } = 1;
public int TotalPages => (int)Math.Ceiling((double)TotalItems / ItemsPerPage);
public int TotalItems { get; private set; }
public bool NoPriorPage => CurrentPage <= 1;
public bool NoNextPage => CurrentPage >= TotalPages;
public int VisibleItemsCount { get; private set; }
public IList<string> SelectedSortableFields => Filter.DocumentProcessingFilter.SortDefinition!.Select(f => f.Field).ToList();
private readonly string[] _sortableFields = { "CreatedDate" };
public IReadOnlyList<string> AvailableSortableFields => _sortableFields
.Except(SelectedSortableFields)
.ToList();
private delegate object GetBusinessViewDocumentInformationValue(BusinessViewDocumentInformation details);
private static readonly IReadOnlyDictionary<string, GetBusinessViewDocumentInformationValue> SortableValues =
new Dictionary<string, GetBusinessViewDocumentInformationValue>
{
{
"CreatedDate",
details => details.DocumentInformation.CreatedDate
},
{
"DocumentType",
details => details.DocumentInformation.DocumentType
},
{
"DocumentBusinessName",
details => details.DocumentInformation.DocumentBusinessName
},
{
"EntityName",
details => details.DocumentInformation.EntityName
}
};
private IDictionary<Guid, DocumentMetadataResponse> _visibleDocuments = new Dictionary<Guid, DocumentMetadataResponse>();
private readonly IModalService _modal;
private readonly IDocumentService _documentService;
private readonly IEntityService _entityService;
private readonly IDocumentTypeService _documentTypeService;
private readonly IJSDocument _jsDocument;
private readonly IDocumentTypeUploaderService _documentTypeUploaderService;
private readonly IToastService _toastService;
private readonly IEntityService _coreApiWrapperService;
private readonly IDocumentProcessingInfoService _documentProcessingInfoService;
public BusinessViewViewModel
(
IModalService modal,
IEntityService entityService,
IDocumentService documentService,
IDocumentTypeService documentTypeService,
IDocumentTypeUploaderService documentTypeUploaderService,
IEntityService coreApiWrapperService,
IToastService toastService,
IJSDocument jsDocument,
AppConfiguration configuration,
IDocumentProcessingInfoService documentProcessingInfoService
)
{
_modal = modal;
_entityService = entityService;
_documentService = documentService;
_documentTypeService = documentTypeService;
_coreApiWrapperService = coreApiWrapperService;
_toastService = toastService;
_jsDocument = jsDocument;
_documentProcessingInfoService = documentProcessingInfoService;
_documentTypeUploaderService = documentTypeUploaderService;
(
Filter.DocumentProcessingFilter.Filter.CreatedDateMinimum,
Filter.DocumentProcessingFilter.Filter.CreatedDateMaximum
) = DefaultFilterDateRange(configuration.DefaultFilters);
}
public async Task GoToPage(int page)
{
var loading = LoadingLighthouse.Show(_modal);
try
{
var documentTypes = _documentTypeService.GetDocumentTypes()
.ContinueWith(r => r.Result.ToDictionary(t => t.DocumentCode));
CurrentPage = page;
Filter.DocumentProcessingFilter.Skip = (CurrentPage - 1) * ItemsPerPage;
Filter.DocumentProcessingFilter.Take = ItemsPerPage;
_visibleDocuments = (await _documentService.GetDocuments(Filter.DocumentProcessingFilter)).ToDictionary(d => d.DocumentId);
VisibleItemsCount = _visibleDocuments.Count;
var logs = _documentService
.GetDocumentLogs(_visibleDocuments.Keys.ToArray())
.ContinueWith(response => response.Result.GroupBy(l => l.DocumentId).ToDictionary(g => g.Key));
var awaitingDocuments = _visibleDocuments.Values.Select(async document =>
{
var accountOwnerships = document.AccountNumber is {} accountNumber
? (await _entityService.GetOwnersByAccountNumber(accountNumber, document.CreatedDate)).ToList()
: await DetermineOwnerByPersonOrOrganizationNumber(document);
var primaryAccountOwnership = GetPrimaryOwner(accountOwnerships);
var entityName = GetPrimaryOwnerName(primaryAccountOwnership);
var entityHasAccess = DoesPrimaryOwnerHaveAccess(primaryAccountOwnership, document.PersonNumbers, document.OrganizationNumbers);
var accountOwners = GetAccountOwners(accountOwnerships);
var personNumbers = new ObservableCollection<string>(document.PersonNumbers ?? Array.Empty<string>());
var organizationNumbers = new ObservableCollection<string>(document.OrganizationNumbers ?? Array.Empty<string>());
var documentAccess = new ObservableCollection<DocumentAccess>();
if (primaryAccountOwnership != null)
{
documentAccess.Add(new DocumentAccess(AccessType.PrimaryOwner, entityHasAccess, entityName,
primaryAccountOwnership.ByOrgNbr == 0
? primaryAccountOwnership.PersonNumber.ToString()
: primaryAccountOwnership.ByOrgNbr.ToString()));
}
else
{
documentAccess.Add(new DocumentAccess(AccessType.PrimaryOwner, true, entityName, "0"));
}
var documentPersonNumbers = document.PersonNumbers?.Where(p => p is not null).ToArray() ?? Array.Empty<string>();
var documentOrganizationNumbers = document.OrganizationNumbers?.Where(o => o is not null).ToArray() ?? Array.Empty<string>();
foreach (var access in GetOwnerAccess(documentPersonNumbers, documentOrganizationNumbers,
accountOwners, entityName))
{
documentAccess.Add(access);
}
foreach (var access in await GetAdditionalAccess(documentPersonNumbers, documentOrganizationNumbers, accountOwners))
{
documentAccess.Add(access);
}
foreach (var access in GetRemovedAccess(documentPersonNumbers, documentOrganizationNumbers, accountOwners, entityName))
{
documentAccess.Add(access);
}
(await documentTypes).TryGetValue(document.Type ?? string.Empty, out var documentType);
var info = new DocumentInformation
(
document.DocumentId,
document.TraceId,
document.Source,
document.DataSource,
document.CreatedDate,
document.UploadDate,
document.Name,
documentType?.DocumentType.ToString() ?? "unknown",
documentType?.DocumentCode ?? "unknown",
documentType?.DocumentBusinessName ?? "unknown",
documentType?.DocumentMemberFacingName ?? "unknown",
documentType?.OnlineBankingRetentionMonths ?? 24,
documentType?.InternalRetentionMonths ?? 84,
entityName,
accountOwners,
documentAccess,
personNumbers,
organizationNumbers,
document.AccountNumber,
documentType?.IsDisabledInMemberView(document) ?? true,
document.ScanStatus,
() => Download(document)
);
return new BusinessViewDocumentInformation
(
info,
(await logs).TryGetValue(document.DocumentId, out var documentLogs)
? documentLogs
: Enumerable.Empty<DocumentAuditResponse>()
);
});
IEnumerable<BusinessViewDocumentInformation> completedDocuments = await Task.WhenAll(awaitingDocuments);
if (VisibleItemsCount == 0)
{
_toastService.ShowWarning("No documents were found with the applied filters");
}
VisibleItems.Clear();
foreach (var document in completedDocuments)
{
VisibleItems.Add(document);
}
DataChanged(this, EventArgs.Empty);
}
catch (Exception exception)
{
DataLoadError(this, EventArgs.Empty);
Log.Error(exception, exception.Message);
}
loading.Close();
}
private async Task<List<AccountOwnership>> DetermineOwnerByPersonOrOrganizationNumber(DocumentMetadataResponse document)
{
var accountOwnerships = new List<AccountOwnership>();
if (EnumExtensions.IsDocumentTypeOf<PrimaryOwnerDocumentTypes>(document.Type))
{
if (document.PersonNumbers?.Count() == 1 && !string.IsNullOrWhiteSpace(document.PersonNumbers.FirstOrDefault()))
{
var person = (await _entityService.GetPersonByEntityNumber(document.PersonNumbers.FirstOrDefault()))
.FirstOrDefault();
if (person != null)
{
accountOwnerships.Add(new AccountOwnership()
{
FirstName = person.FirstName,
MiddleInitial = person.MiddleInitial,
MiddleName = person.MiddleName,
LastName = person.LastName,
Suffix = person.Suffix,
PersonNumber = person.PersonNumber,
IsPrimaryOwner = true
});
}
}
else if (document.OrganizationNumbers?.Count() == 1 &&
!string.IsNullOrWhiteSpace(document.OrganizationNumbers.FirstOrDefault()))
{
var organization =
(await _entityService.GetOrganizationByEntityNumber(document.OrganizationNumbers.FirstOrDefault()))
.FirstOrDefault();
if (organization != null)
{
accountOwnerships.Add(new AccountOwnership()
{
OrganizationName = organization.Name,
ByOrgNbr = organization.OrganizationNumber,
IsPrimaryOwner = true
});
}
}
}
else if (EnumExtensions.IsDocumentTypeOf<AccountDocumentTypes>(document.Type))
{
var ownersByAccountNumber =
await _entityService.GetOwnersByAccountNumber(document.AccountNumber.Value, document.CreatedDate);
accountOwnerships = ownersByAccountNumber?.ToList();
}
return accountOwnerships;
}
private Dictionary<string, Dictionary<string, string>> GetAccountOwners(IEnumerable<AccountOwnership> accountOwnerships)
{
var distinctAccountOwners = new Dictionary<string, Dictionary<string, string>>
{
["persons"] = new Dictionary<string, string>(),
["organizations"] = new Dictionary<string, string>()
};
if (accountOwnerships == null)
{
return distinctAccountOwners;
}
foreach (var owner in accountOwnerships)
{
if(owner.ByOrgNbr != 0)
{
var key = owner.ByOrgNbr.ToString();
var value = GetName(owner);
if (!distinctAccountOwners["organizations"].ContainsKey(key))
{
distinctAccountOwners["organizations"].Add(key, value);
}
}
else
{
var key = owner.PersonNumber.ToString();
var value = GetName(owner);
if (!distinctAccountOwners["persons"].ContainsKey(key))
{
distinctAccountOwners["persons"].Add(key, value);
}
}
}
return distinctAccountOwners;
}
private static AccountOwnership? GetPrimaryOwner(IEnumerable<AccountOwnership> accountOwnerships)
{
return accountOwnerships.FirstOrDefault(x => x.IsPrimaryOwner);
}
private string GetPrimaryOwnerName(AccountOwnership? primaryAccountOwnership)
{
if(primaryAccountOwnership != null)
{
return GetName(primaryAccountOwnership);
}
else
{
return "No primary owner found";
}
}
private List<DocumentAccess> GetOwnerAccess(string[] personNumbers, string[] organizationNumbers, Dictionary<string, Dictionary<string, string>> accounts, string entityName)
{
var access = new List<DocumentAccess>();
if(!accounts.Any())
{
return access;
}
if (personNumbers != null)
{
foreach (var account in accounts["persons"].Where(x => personNumbers.Contains(x.Key)))
{
if(!account.Value.Equals(entityName, StringComparison.OrdinalIgnoreCase))
{
access.Add(new DocumentAccess(AccessType.Owner, true, account.Value, account.Key));
}
}
}
if (organizationNumbers != null)
{
foreach (var account in accounts["organizations"].Where(x => organizationNumbers.Contains(x.Key)))
{
if (!account.Value.Equals(entityName, StringComparison.OrdinalIgnoreCase))
{
access.Add(new DocumentAccess(AccessType.Owner, true, account.Value, account.Key));
}
}
}
return access;
}
private async Task<List<DocumentAccess>> GetAdditionalAccess(string[] personNumbers, string[] organizationNumbers, Dictionary<string, Dictionary<string, string>> accounts)
{
var access = new List<DocumentAccess>();
foreach (var memberNumber in personNumbers.Where(x => !string.IsNullOrWhiteSpace(x) && !accounts["persons"].ContainsKey(x)))
{
var personEntity = (await _entityService.GetPersonByEntityNumber(memberNumber)).FirstOrDefault();
if (personEntity != null)
{
access.Add(new DocumentAccess(AccessType.Additional, true, personEntity.NameFull, personEntity.PersonNumber.ToString()));
}
}
foreach (var memberNumber in organizationNumbers.Where(x => !string.IsNullOrWhiteSpace(x) && !accounts["organizations"].ContainsKey(x)))
{
var orgEntity = (await _entityService.GetOrganizationByEntityNumber(memberNumber)).FirstOrDefault();
if (orgEntity != null)
{
access.Add(new DocumentAccess(AccessType.Additional, true, orgEntity.Name, orgEntity.OrganizationNumber.ToString()));
}
}
return access;
}
private List<DocumentAccess> GetRemovedAccess(string[] personNumbers, string[] organizationNumbers, Dictionary<string, Dictionary<string, string>> accounts, string entityName)
{
var access = new List<DocumentAccess>();
if (!accounts["persons"].Any() && !accounts["organizations"].Any())
{
return access;
}
if (personNumbers == null)
{
personNumbers = Array.Empty<string>();
}
if (organizationNumbers == null)
{
organizationNumbers = Array.Empty<string>();
}
foreach (var account in accounts["persons"].Where(x => !personNumbers.Contains(x.Key)))
{
if (!account.Value.Equals(entityName, StringComparison.OrdinalIgnoreCase))
{
access.Add(new DocumentAccess(AccessType.Removed, false, account.Value, account.Key));
}
}
foreach (var account in accounts["organizations"].Where(x => !organizationNumbers.Contains(x.Key)))
{
if (!account.Value.Equals(entityName, StringComparison.OrdinalIgnoreCase))
{
access.Add(new DocumentAccess(AccessType.Removed, false, account.Value, account.Key));
}
}
return access;
}
private static bool DoesPrimaryOwnerHaveAccess(AccountOwnership? primaryAccountOwnership, ICollection<string> personNumbers, ICollection<string> organizationNumbers)
{
var primaryOwnerHasAccess = false;
if (primaryAccountOwnership != null)
{
if (personNumbers != null && personNumbers.Contains(primaryAccountOwnership.PersonNumber.ToString()))
{
primaryOwnerHasAccess = true;
}
if (organizationNumbers != null && organizationNumbers.Contains(primaryAccountOwnership.ByOrgNbr.ToString()))
{
primaryOwnerHasAccess = true;
}
}
else
{
// No primary owner should return true so the value does not have a strike-through on the view
primaryOwnerHasAccess = true;
}
return primaryOwnerHasAccess;
}
private string GetName(AccountOwnership account)
{
if(account.ByOrgNbr == 0)
{
var name = account.FirstName + " ";
if(!String.IsNullOrEmpty(account.MiddleName))
{
name += account.MiddleName + " ";
}
else if(!String.IsNullOrEmpty(account.MiddleInitial))
{
name += account.MiddleInitial + ". ";
}
name += account.LastName;
return name;
}
return account.OrganizationName;
}
public async Task OnItemsPerPageValueChanged(ChangeEventArgs e)
{
Filter.DocumentProcessingFilter.Take = ItemsPerPage = int.Parse(e.Value!.ToString()!);
Filter.DocumentProcessingFilter.Skip = 0;
await Refresh();
}
public async Task Refresh()
{
try
{
TotalItems = (int)await _documentService.CountDocuments(Filter.DocumentProcessingFilter.Filter);
Loaded = true;
await GoToPage(1);
}
catch { }
}
public async Task ApplyFilterAndSorting()
{
var loading = LoadingLighthouse.Show(_modal);
Filter.DocumentProcessingFilter.Skip = 0;
Filter.DocumentProcessingFilter.Filter.PersonNumbersContainsAny = null;
Filter.DocumentProcessingFilter.Filter.OrganizationNumbersContainsAny = null;
Filter.DocumentProcessingFilter.Filter.AccountNumberContainsAny = null;
var shouldRefresh = true;
if (Filter.OwnershipType != null &&
(!string.IsNullOrWhiteSpace(Filter.OwnershipValue) || !string.IsNullOrWhiteSpace(Filter.AdditionalOwnershipValue)))
{
switch (Filter.OwnershipType)
{
case OwnershipTypes.MemberNumber:
try
{
var member = await _coreApiWrapperService.GetPersonOrOrganizationNumberByMemberId((long)Convert.ToDouble(Filter.OwnershipValue));
if (member is { PersonNumber: { } personNumber })
{
Filter.DocumentProcessingFilter.Filter.PersonNumbersContainsAny = new[] { personNumber };
}
if (member is { OrganizationNumber: { } organizationNumber })
{
Filter.DocumentProcessingFilter.Filter.OrganizationNumbersContainsAny = new[] { organizationNumber };
}
break;
}
catch
{
shouldRefresh = false;
break;
}
case OwnershipTypes.AccountNumber:
Filter.DocumentProcessingFilter.Filter.AccountNumberContainsAny = new [] { Filter.OwnershipValue };
break;
case OwnershipTypes.PersonName:
var request = new PersonNameRequest() { FirstName = Filter.OwnershipValue ?? "", LastName = Filter.AdditionalOwnershipValue ?? "", MdlName = "" };
const int personLimit = 50;
try
{
var personCount = await _coreApiWrapperService.GetCountPersonsByNameLike(request);
if (personCount > personLimit)
{
_toastService.ShowWarning(
$"There would be {personCount} persons in your search.\nResults have been limited to {personLimit}.\nIf you are able, please search with a more specific name.");
}
var persons = await _coreApiWrapperService.GetPersonsByNameLike(request, personLimit);
if (!persons.Any())
{
_toastService.ShowWarning("No person(s) were found.");
shouldRefresh = false;
}
Filter.DocumentProcessingFilter.Filter.PersonNumbersContainsAny =
persons.Select(x => x.PersonNumber.ToString()).ToArray();
break;
}
catch
{
shouldRefresh = false;
break;
}
case OwnershipTypes.OrganizationName:
//50 is near the query string limit for filtering
//to raise the limit chunk the request and concatenate the results like getting document audit logs
const int organizationLimit = 50;
try
{
var checkLimit = await _coreApiWrapperService.CountOrganizationNumbersByName(Filter.OwnershipValue)
.ContinueWith(async count =>
{
if (count.Result > organizationLimit)
{
_toastService.ShowWarning(
$"There would be {await count} organizations in your search.\nResults have been limited to {organizationLimit}.\nIf you are able, please search with a more specific name.");
}
});
var organizations = await _coreApiWrapperService.GetOrganizationByOrganizationName(Filter.OwnershipValue, organizationLimit);
await checkLimit;
if (!organizations.Any())
{
_toastService.ShowWarning("No organization(s) were found.");
shouldRefresh = false;
}
Filter.DocumentProcessingFilter.Filter.OrganizationNumbersContainsAny =
organizations.Select(x => x.OrganizationNumber.ToString()).ToArray();
break;
}
catch
{
shouldRefresh = false;
break;
}
case OwnershipTypes.OrganizationNumber:
Filter.DocumentProcessingFilter.Filter.OrganizationNumbersContainsAny = new[] { Filter.OwnershipValue };
break;
case OwnershipTypes.PersonNumber:
Filter.DocumentProcessingFilter.Filter.PersonNumbersContainsAny = new[] { Filter.OwnershipValue };
break;
default:
throw new NotImplementedException();
}
}
loading.Close();
if(shouldRefresh) await Refresh();
}
public async Task ClearFilterAndSorting()
{
Filter.OwnershipType = null;
Filter.OwnershipValue = null;
Filter.DocumentProcessingFilter = new DocumentInformationFilteredSearch
{
Filter = new DocumentInformationFilter(),
SortDefinition = new List<SortableField>()
};
await Refresh();
}
public async Task SortFieldsSelected(IList<string> fields)
{
Filter.DocumentProcessingFilter.SortDefinition = fields
.Select((fieldName, position) =>
{
var field = Filter.DocumentProcessingFilter.SortDefinition!
.FirstOrDefault(f => f.Field == fieldName)
?? new SortableField { Field = fieldName };
field.Position = position;
return field;
})
.ToList();
await ApplyFilterAndSorting();
}
public async Task SortFieldRemoved(string field)
{
foreach (var sortField in Filter.DocumentProcessingFilter.SortDefinition!.Where(f => f.Field == field))
{
Filter.DocumentProcessingFilter.SortDefinition!.Remove(sortField);
}
await ApplyFilterAndSorting();
}
public Task SetFieldDescending(string field) => SortField(field, true);
public Task SetFieldAscending(string field) => SortField(field, false);
private async Task SortField(string field, bool descending)
{
Filter.DocumentProcessingFilter.SortDefinition!.Single(f => f.Field == field).IsDescending = descending;
await ApplyFilterAndSorting();
}
public bool IsFieldDescending(string field) => Filter.DocumentProcessingFilter.SortDefinition!.Single(f => f.Field == field).IsDescending;
private async Task Download(DocumentMetadataResponse document)
{
try
{
if (document.DocumentBlobId is null)
{
return;
}
var content = (await _documentService.StreamDocumentContent(document.DocumentBlobId ?? Guid.Empty)).Replace("\"", string.Empty);
var byteContent = Convert.FromBase64String(content);
using var contentStream = new DotNetStreamReference
(
new MemoryStream(byteContent)
);
await _documentService.ExternalActionPerformed(document.DocumentId, DocumentAction.Download, "Business View");
await _jsDocument.DownloadDocument($"{FileReader.GetExtensionByBytes(byteContent, document.Name)}", contentStream);
}
catch { }
}
public void UpdateAuthorizationLists(DocumentInformation document, EntityTypes selectedEntityType, string dnaEntityNameResult, string dnaEntityIdResult)
{
var removed = document.DocumentAccess.FirstOrDefault(x => x.AccessType == AccessType.Removed && x.EntityName.Equals(dnaEntityNameResult));
if (removed != null)
{
document.DocumentAccess.Remove(removed);
}
if (document.EntityName.Equals(dnaEntityNameResult))
{
var primaryOwner = document.DocumentAccess.Where(x => x.EntityName.Equals(dnaEntityNameResult)).FirstOrDefault();
if (primaryOwner != null)
{
document.DocumentAccess.Remove(primaryOwner);
}
document.DocumentAccess.Add(new DocumentAccess(AccessType.PrimaryOwner, true, dnaEntityNameResult, dnaEntityIdResult));
}
else if (selectedEntityType == EntityTypes.Person && document.AccountOwners["persons"].ContainsKey(dnaEntityIdResult) ||
selectedEntityType == EntityTypes.Organization && document.AccountOwners["organizations"].ContainsKey(dnaEntityIdResult))
{
document.DocumentAccess.Add(new DocumentAccess(AccessType.Owner, true, dnaEntityNameResult, dnaEntityIdResult));
}
else
{
document.DocumentAccess.Add(new DocumentAccess(AccessType.Additional, true, dnaEntityNameResult, dnaEntityIdResult));
}
}
public void PrepareForPrimaryOwnerDocumentRequest(DocumentInformation document, EntityTypes selectedEntityType, string dnaEntityNameResult, string dnaEntityIdResult)
{
List<DocumentAccess> accesses = new List<DocumentAccess>();
foreach (var account in document.AccountOwners["persons"])
{
if (document.EntityName.Equals(account.Value))
{
accesses.Add(new DocumentAccess(AccessType.PrimaryOwner, false, account.Value, account.Key));
}
else
{
accesses.Add(new DocumentAccess(AccessType.Removed, false, account.Value, account.Key));
}
}
foreach (var account in document.AccountOwners["organizations"])
{
if (document.EntityName.Equals(account.Value))
{
accesses.Add(new DocumentAccess(AccessType.PrimaryOwner, false, account.Value, account.Key));
}
else
{
accesses.Add(new DocumentAccess(AccessType.Removed, false, account.Value, account.Key));
}
}
document.DocumentAccess.Clear();
document.PersonNumbers.Clear();
document.OrganizationNumbers.Clear();
if (selectedEntityType == EntityTypes.Person)
{
document.PersonNumbers.Add(dnaEntityIdResult);
}
else
{
document.OrganizationNumbers.Add(dnaEntityIdResult);
}
foreach (var access in accesses)
{
document.DocumentAccess.Add(access);
}
}
public async Task DocumentMetadataUpdate(DocumentInformation document, EntityTypes selectedEntityType, string dnaEntityIdResult)
=> await _documentTypeUploaderService.DocumentMetadataUpdate(document, selectedEntityType, dnaEntityIdResult);
public void RemoveAuthorizationFromDocument(DocumentInformation document, string dnaEntityNameResult, string dnaEntityIdResult)
{
var primaryowner = document.DocumentAccess.Where(x => x.AccessType == AccessType.PrimaryOwner && x.EntityName.Equals(dnaEntityNameResult)).FirstOrDefault();
if (primaryowner != null)
{
document.DocumentAccess.Remove(primaryowner);
document.DocumentAccess.Add(new DocumentAccess(AccessType.PrimaryOwner, false, dnaEntityNameResult, dnaEntityIdResult));
}
var owner = document.DocumentAccess.Where(x => x.AccessType == AccessType.Owner && x.EntityName.Equals(dnaEntityNameResult)).FirstOrDefault();
if (owner != null)
{
document.DocumentAccess.Remove(owner);
document.DocumentAccess.Add(new DocumentAccess(AccessType.Removed, false, dnaEntityNameResult, dnaEntityIdResult));
}
var additional = document.DocumentAccess.Where(x => x.AccessType == AccessType.Additional && x.EntityName.Equals(dnaEntityNameResult)).FirstOrDefault();
if (additional != null)
{
document.DocumentAccess.Remove(additional);
}
}
public async Task<Person?> GetPersonByPersonNumber(string DnaEntityIdSearch)
{
try
{
return (await _entityService.GetPersonByEntityNumber(DnaEntityIdSearch)).FirstOrDefault();
}
catch (Exception exception)
{
Log.Error(exception, exception.Message);
}
return null;
}
public async Task<Organization?> GetOrganizationByOrganizationNumber(string DnaEntityIdSearch)
{
try
{
return (await _entityService.GetOrganizationByEntityNumber(DnaEntityIdSearch)).FirstOrDefault();
}
catch (Exception exception)
{
Log.Error(exception, exception.Message);
}
return null;
}
private static (DateTime CreationDateStart, DateTime CreationDateEnd) DefaultFilterDateRange(DefaultFilters configuration)
=> (
DateTime.Now.Date.AddDays(configuration.CreationDateStartOffsetDays),
DateTime.Now.Date.AddDays(configuration.CreationDateEndOffsetDays).AddDays(1).AddTicks(-1)
);
}
public static class FileReader
{
private static readonly byte[] Bmp = { 66, 77 };
private static readonly byte[] Doc = { 208, 207, 17, 224, 161, 177, 26, 225 };
private static readonly byte[] ExeDll = { 77, 90 };
private static readonly byte[] Gif = { 71, 73, 70, 56 };
private static readonly byte[] Ico = { 0, 0, 1, 0 };
private static readonly byte[] Jpg = { 255, 216, 255 };
private static readonly byte[] Mp3 = { 255, 251, 48 };
private static readonly byte[] Pdf = { 37, 80, 68, 70, 45, 49, 46 };
private static readonly byte[] Png = { 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82 };
private static readonly byte[] Rar = { 82, 97, 114, 33, 26, 7, 0 };
private static readonly byte[] Swf = { 70, 87, 83 };
private static readonly byte[] Tiff = { 73, 73, 42, 0 };
private static readonly byte[] Ttf = { 0, 1, 0, 0, 0 };
private static readonly byte[] Avi = { 82, 73, 70, 70 };
private static readonly byte[] Wma = { 48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108 };
private static readonly byte[] DocX = { 80, 75, 3, 4 };
public static string GetExtensionByBytes(byte[] file, string fileName)
{
if (!string.IsNullOrWhiteSpace(Path.GetExtension(fileName))) return fileName;
if (file.Take(2).SequenceEqual(Bmp)) return $"{fileName}.bmp";
if (file.Take(2).SequenceEqual(ExeDll)) return $"{fileName}.exe";
if (file.Take(3).SequenceEqual(Jpg)) return $"{fileName}.jpg";
if (file.Take(3).SequenceEqual(Mp3)) return $"{fileName}.mp3";
if (file.Take(3).SequenceEqual(Swf)) return $"{fileName}.swf";
if (file.Take(4).SequenceEqual(Avi)) return $"{fileName}.avi";
if (file.Take(4).SequenceEqual(DocX)) return $"{fileName}.docx";
if (file.Take(4).SequenceEqual(Gif)) return $"{fileName}.gif";
if (file.Take(4).SequenceEqual(Ico)) return $"{fileName}.ico";
if (file.Take(4).SequenceEqual(Tiff)) return $"{fileName}.tiff";
if (file.Take(5).SequenceEqual(Ttf)) return $"{fileName}.ttf";
if (file.Take(7).SequenceEqual(Pdf)) return $"{fileName}.pdf";
if (file.Take(7).SequenceEqual(Rar)) return $"{fileName}.rar";
if (file.Take(8).SequenceEqual(Doc)) return $"{fileName}.doc";
if (file.Take(16).SequenceEqual(Png)) return $"{fileName}.png";
if (file.Take(16).SequenceEqual(Wma)) return $"{fileName}.wma";
return fileName;
}
}
@@ -0,0 +1,211 @@
@using LCU.Library.Internal.DocumentIngestorModels.Models
@inject AuthenticationHelper AuthorizationHelper;
@inject LCU.Library.Internal.Security.Service.IAzureAdAuthorizationService AzureAdAuthorizationService
@inject IModalService Modal;
@inject IDocumentProcessingInfoService DocumentProcessingInfoService;
@inject IToastService ToastService;
@if (processStatusTypes.Any()) {
<CollapsibleComponent ButtonClass="btn btn-link"
HeaderClass="row grid-header-row grid-subheader-row-container processing-header"
ButtonContainerClass="grid-button-container"
IsCollapsed="false">
<HeaderContent>
<RowSubHeader>Update Process Status</RowSubHeader>
</HeaderContent>
<BodyContent>
<Confirm @ref="UpdateStatusConfirmation" ConfirmationChanged="UpdateStatus_Confirm"
ConfirmationMessage=@("Are you sure you want to update the processing status" +
$" from {DocumentProcessingInfo.StatusType} to {SelectedStatus}?")
ConfirmationTitle=@("Update Status")>
</Confirm>
<div class="d-flex grid-row-container-bordered">
<div class="col-md-3 col-with-right-boarder">
<div class="form-group form-row">
<label for="StatusTypes" class="col-sm-12 col-form-label">Change Status to:</label>
<select class="form-control" id="StatusTypes" @bind="SelectedStatus" @oninput="ChangeStatusType">
<option></option>
@foreach (var status in processStatusTypes)
{
<option value="@status">@status</option>
}
</select>
</div>
</div>
<div class="col-md-6 col-with-right-boarder">
<div class="form-group form-row">
<label for="ReasonForChange" class="col-sm-12 col-form-label">Reason for Change:</label>
<input class="form-control" id="ReasonForChange" placeholder="" @oninput="ChangeReasonMessage" @bind="UpdateStatusRequest.ReasonMessage"/>
</div>
</div>
<div class="col-md-3">
<div>
<label for="UpdateStatus" class="col-sm-12 col-form-label">&nbsp;</label>
<button type="button"
class="btn btn-primary"
@onclick="UpdateStatus_Click"
title="Update Status"
id="UpdateStatus"
disabled="@Disabled">
Update Process Status
</button>
</div>
</div>
</div>
</BodyContent>
</CollapsibleComponent>
}
@code {
[Parameter]
public DocumentProcessingInfo DocumentProcessingInfo { get; set; }
[Parameter]
public EventCallback<IModalReference> RefreshData { get; set; }
[Parameter]
public DocumentProcessingInfoStatusRequest UpdateStatusRequest { get; set; }
[Parameter]
public IEnumerable<ProcessStatusTypes> processStatusTypes { get; set; }
[Parameter]
public bool Disabled { get; set; } = true;
private ConfirmBase? UpdateStatusConfirmation { get; set; }
private ProcessStatusTypes? SelectedStatus { get; set; } = null;
private readonly Dictionary<ProcessStatusTypes, List<ProcessStatusTypes>> _statusChangeRejectionList =
new ()
{
[ProcessStatusTypes.Processing] = new()
{
ProcessStatusTypes.Reprocessing,
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.Completed] = new()
{
ProcessStatusTypes.Processing,
ProcessStatusTypes.Completed,
ProcessStatusTypes.ArchiveFailure,
ProcessStatusTypes.CriticalFailure,
ProcessStatusTypes.DataMappingFailure,
ProcessStatusTypes.Reprocessing,
ProcessStatusTypes.NotificationDataFailure,
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.ArchiveFailure] = new()
{
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.CriticalFailure] = new()
{
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.DataMappingFailure] = new()
{
ProcessStatusTypes.Processing,
ProcessStatusTypes.Completed,
ProcessStatusTypes.ArchiveFailure,
ProcessStatusTypes.CriticalFailure,
ProcessStatusTypes.DataMappingFailure,
ProcessStatusTypes.Reprocessing,
ProcessStatusTypes.NotificationDataFailure,
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.Reprocessing] = new()
{
ProcessStatusTypes.Processing,
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.NotificationDataFailure] = new()
{
ProcessStatusTypes.Processing,
ProcessStatusTypes.ArchiveFailure,
ProcessStatusTypes.CriticalFailure,
ProcessStatusTypes.DataMappingFailure,
ProcessStatusTypes.Reprocessing,
ProcessStatusTypes.UploadCompletionReprocessing
},
[ProcessStatusTypes.UploadCompletionReprocessing] = new()
{
ProcessStatusTypes.Processing,
ProcessStatusTypes.ArchiveFailure,
ProcessStatusTypes.CriticalFailure,
ProcessStatusTypes.DataMappingFailure,
ProcessStatusTypes.Reprocessing
}
};
protected override void OnInitialized()
{
processStatusTypes = GetStatusTypes();
UpdateStatusRequest = new DocumentProcessingInfoStatusRequest()
{
DocumentProcessingInfoId = DocumentProcessingInfo.DocumentProcessingInfoId,
ProcessStatus = DocumentProcessingInfo.StatusType,
Username = AzureAdAuthorizationService.GetUserName()
};
}
private IEnumerable<ProcessStatusTypes> GetStatusTypes()
{
return Enum.GetValues<ProcessStatusTypes>()
.Where(x => !_statusChangeRejectionList[DocumentProcessingInfo.StatusType].Contains(x)
&& x != DocumentProcessingInfo.StatusType);
}
private void ChangeStatusType(ChangeEventArgs changeEventArgs)
{
var value = changeEventArgs.Value?.ToString();
if(!string.IsNullOrWhiteSpace(value))
{
SelectedStatus = UpdateStatusRequest.ProcessStatus = Enum.Parse<ProcessStatusTypes>(value);
}
else
{
SelectedStatus = null;
}
ValidateForm();
}
private void ChangeReasonMessage(ChangeEventArgs changeEventArgs)
{
UpdateStatusRequest.ReasonMessage = changeEventArgs.Value?.ToString();
ValidateForm();
}
private void ValidateForm()
{
Disabled = string.IsNullOrWhiteSpace(UpdateStatusRequest.ReasonMessage) ||
string.IsNullOrWhiteSpace(UpdateStatusRequest.Username) ||
SelectedStatus is null;
}
private void UpdateStatus_Click()
{
UpdateStatusConfirmation?.Show();
}
private async Task UpdateStatus_Confirm(bool confirmed)
{
if (confirmed)
{
var openModal = LoadingLighthouse.Show(Modal);
try
{
await DocumentProcessingInfoService.UpdateStatus(UpdateStatusRequest);
ToastService.ShowInfo("Document status has been updated.");
}
catch {}
await RefreshData.InvokeAsync(openModal);
}
}
}
@@ -0,0 +1,28 @@
@using DocumentTypeRetriever
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@inject IModalService Modal;
<AuthorizeView Policy="@PolicyConstants.DocCodeDetailsCreate">
<button type="button"
class="btn btn-primary"
title="New"
@onclick="@ShowNewDocCodeDetailsModal"
style="margin-left: -0.5rem;">
New
</button>
</AuthorizeView>
@code {
[Parameter]
public CreateDocumentCodeDetails CreateDocumentCodeDetails { get; set; }
private async Task ShowNewDocCodeDetailsModal()
{
var modal = await DocCodeDetailsCreationModal.Show(Modal).Result;
if (modal.Confirmed && modal.Data is DocumentCodeDetailsRequest newDetails)
{
await CreateDocumentCodeDetails(newDetails);
}
}
}
@@ -0,0 +1,246 @@
@using System.ComponentModel.DataAnnotations
@using DocumentTypeRetriever
@using DocumentTypes = DocumentTypeRetriever.DocumentTypes
@inject IJSTooltips JsTooltips;
<EditForm Model="@_newDetails" OnValidSubmit="@ReturnNewDetails">
<div class="row py-2">
<div class="col-12">
<label for="document-code" class="required text-nowrap">Document Code:</label>
</div>
<div class="col-12">
<InputText id="document-code"
class="form-control flex-fill"
@oninput="@(async change => {_newDetails.DocumentCode = change.Value!.ToString(); await Validate(change);})"
@bind-Value="@_newDetails.DocumentCode"/>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.DocumentCode)"/>
</span>
</div>
<div class="row py-2">
<div class="col-12">
<label for="document-business-name" class="required text-no-wrap">Document Business Name:</label>
</div>
<div class="col-12">
<InputText id="document-business-name"
class="form-control flex-fill"
@oninput="@(async change => {_newDetails.DocumentBusinessName = change.Value!.ToString(); await Validate(change);})"
@bind-Value="@_newDetails.DocumentBusinessName"/>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.DocumentBusinessName)"/>
</span>
</div>
<div class="row py-2">
<div class="col-12">
<label for="document-member-facing-name" class="required text-no-wrap">Document Member Facing Name:</label>
</div>
<div class="col-12">
<InputText id="document-member-facing-name"
class="form-control flex-fill"
@oninput="@(async change => {_newDetails.DocumentMemberFacingName = change.Value!.ToString(); await Validate(change);})"
@bind-Value="@_newDetails.DocumentMemberFacingName"/>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.DocumentMemberFacingName)"/>
</span>
</div>
<div class="row py-2">
<div class="col">
<label for="document-type" class="required text-no-wrap">Document Type:</label>
</div>
<div class="col-5">
<InputSelect id="document-type"
class="form-control flex-fill"
@bind-Value="@_newDetails.DocumentType">
@foreach (var option in Enum.GetValues<DocumentTypes>())
{
<option value="@option">@option</option>
}
</InputSelect>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.DocumentType)"/>
</span>
</div>
<div class="row py-2">
<div class="col">
<label for="document-disable-member-view" class="text-nowrap">
Disable Member View <i class="fa fa-info-circle" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="If true documents of this type will be disabled in member view regardless of start or end date."></i>:
</label>
</div>
<div class="col-3 justify-content-center">
<InputCheckbox id="document-disable-member-view" @bind-Value="@_newDetails.DisableMemberView"/>
</div>
</div>
<div class="row py-2">
<div class="col">
<label for="document-disable-member-view-start-date" class="text-nowrap">
Disable Member View Start <i class="fa fa-info-circle" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="This type of document will be disabled in member view from this date to the disable end date only if there is also an end date."></i>:
</label>
</div>
<div class="col-5">
<InputDate id="document-disable-member-view-start-date"
class="form-control flex-fill"
@oninput="@(async change => {
_newDetails.DisableMemberViewStartDate = Convert.ToDateTime(change.Value);
await Validate(change);
})"
@bind-Value="@_newDetails.DisableMemberViewStartDate"/>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.DisableMemberViewStartDate)"/>
</span>
</div>
<div class="row py-2">
<div class="col">
<label for="document-disable-member-view-end-date" class="text-nowrap">
Disable Member View End <i class="fa fa-info-circle" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="This type of document will be disabled in member from the start date until this date if there is a start date. No value will result in 'start date - Forever'."></i>:
</label>
</div>
<div class="col-5">
<InputDate id="document-disable-member-view-end-date"
class="form-control flex-fill"
@oninput="@(async change => {
_newDetails.DisableMemberViewEndDate = Convert.ToDateTime(change.Value);
await Validate(change);
})"
@bind-Value="@_newDetails.DisableMemberViewEndDate"/>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.DisableMemberViewEndDate)"/>
</span>
</div>
<div class="row py-2">
<div class="col">
<label for="online-banking-retention-months" class="required text-no-wrap">Online Retention:</label>
</div>
<div class="col-5">
<InputNumber id="online-banking-retention-months"
TValue="int?"
class="form-control flex-fill col-5"
style="display: inline-block"
@oninput="@(async change => {
_newDetails.OnlineBankingRetentionMonths = int.TryParse(change.Value?.ToString(), out var x) ? x : null;
await Validate(change);
})"
@bind-Value="@_newDetails.OnlineBankingRetentionMonths"/>
<span class="mt-2 col">months</span>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.OnlineBankingRetentionMonths)"/>
</span>
</div>
<div class="row py-2">
<div class="col">
<label for="internal-retention-months" class="required text-no-wrap">Internal Retention:</label>
</div>
<div class="col-5">
<InputNumber id="internal-retention-months"
TValue="int?"
class="form-control flex-fill col-5"
style="display: inline-block"
@oninput="@(async change => {
_newDetails.InternalRetentionMonths = int.TryParse(change.Value?.ToString(), out var x) ? x : null;
await Validate(change);
})"
@bind-Value="@_newDetails.InternalRetentionMonths"/>
<span class="mt-2 col">months</span>
</div>
<span class="col-12">
<ValidationMessage For="@(() => _newDetails.InternalRetentionMonths)"/>
</span>
</div>
<div class="row pt-2 flex-row-reverse">
<div class="col-auto">
<button type="submit" class="btn btn-primary mr-1" disabled="@_saveIsDisabled">Submit</button>
<button type="button" class="btn btn-secondary py-2" @onclick="@Cancel">Cancel</button>
</div>
</div>
<FluentValidationValidator @ref="_fluentValidationValidator" Options="@(options => options.IncludeRuleSets(_ruleSets))"/>
</EditForm>
@code {
[CascadingParameter]
public BlazoredModalInstance BlazoredModal { get; set; }
public static IModalReference Show(IModalService modal)
{
var options = new ModalOptions
{
Position = ModalPosition.Middle
};
return modal.Show<DocCodeDetailsCreationModal>("New Doc Code Details", options);
}
private readonly DocumentCodeDetails _newDetails = new();
private FluentValidationValidator? _fluentValidationValidator;
private readonly string[] _ruleSets = {"DocumentCodeDetails"};
private bool _saveIsDisabled = true;
public class DocumentCodeDetails
{
[Required]
public string DocumentCode { get; set; }
[Required]
public string DocumentBusinessName { get; set; }
[Required]
public string DocumentMemberFacingName { get; set; }
[Required]
public DocumentTypes DocumentType { get; set; }
public bool DisableMemberView { get; set; }
public DateTime? DisableMemberViewStartDate { get; set; }
public DateTime? DisableMemberViewEndDate { get; set; }
public int? OnlineBankingRetentionMonths { get; set; } = 24;
public int? InternalRetentionMonths { get; set; } = 84;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await JsTooltips.Enable();
await base.OnAfterRenderAsync(firstRender);
}
private async Task ReturnNewDetails()
{
var newDetails = new DocumentCodeDetailsRequest
{
DocumentCode = _newDetails.DocumentCode,
DocumentBusinessName = _newDetails.DocumentBusinessName,
DocumentMemberFacingName = _newDetails.DocumentMemberFacingName,
DocumentType = _newDetails.DocumentType,
DisableMemberView = _newDetails.DisableMemberView,
DisableMemberViewStartDate = _newDetails.DisableMemberViewStartDate,
DisableMemberViewEndDate = _newDetails.DisableMemberViewEndDate,
OnlineBankingRetentionMonths = _newDetails.OnlineBankingRetentionMonths ?? 24,
InternalRetentionMonths = _newDetails.InternalRetentionMonths ?? 84
};
await BlazoredModal.CloseAsync(ModalResult.Ok(newDetails));
}
private async Task Cancel()
{
await BlazoredModal.CancelAsync();
}
private async Task Validate(EventArgs args)
{
_saveIsDisabled = !await _fluentValidationValidator!.ValidateAsync();
}
}
@@ -0,0 +1,148 @@
@using LCU.WebApps.Internal.DocumentManager.Models.Constants
@inject IJSTooltips JSTooltips;
@inject AuthenticationHelper Auth;
<RowSubHeader>Document Attributes</RowSubHeader>
<div class="container-fluid">
<div class="col">
<div class="row font-weight-bold">
Customer Facing Name:
</div>
<div class="row">
@CustomerFacingName
</div>
<div class="row font-weight-bold">
Account Number:
</div>
<div class="row">
@(AccountNumber ?? "Unknown")
</div>
<div class="row font-weight-bold">
Entity Name:
</div>
<div class="row">
@EntityName
</div>
<AuthorizeView Policy="@PolicyConstants.DocumentLookupShowDocumentId">
<div class="row font-weight-bold">
Document ID:
</div>
<div class="row">
@DocumentId
</div>
</AuthorizeView>
@if (!VisibleInMemberView)
{
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-1 align-self-center text-center font-weight-bold doc-type-disabled-warning">!</div>
<div class="col">This document is currently hidden from the member portal</div>
</div>
}
@if (!String.IsNullOrEmpty(RetentionMessage))
{
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-1 align-self-center text-center font-weight-bold doc-type-disabled-warning">!</div>
<div class="col">
<p class="mb-1">@RetentionMessage</p>
<p class="mb-0">Authorizations cannot be modified.</p>
</div>
</div>
}
@if (ScanStatus == false)
{
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-1 p-0 align-self-center text-center font-weight-bold doc-type-disabled-warning">
<i class="fa fa-warning"></i>
</div>
<div class="col">
<p>Security scan marked this file as malicious and it may not be downloaded or updated.</p>
</div>
</div>
}
@if (!DateIsAfterInternalRetentionDate() && ScanStatus != false && IsAllowedToDownload)
{
<div class="row justify-content-center mt-4 mb-2">
<button type="button" class="btn btn-primary" @onclick="DownloadDocument" title="Download" disabled="@(!Scanned)">
Download
@if (!Scanned)
{
<i class="fa fa-info-circle" style="cursor: pointer;" data-toggle="tooltip" data-placement="top" title="This document cannot be downloaded, it is queued for a security scan."></i>
}
</button>
</div>
}
</div>
</div>
@code {
[Parameter]
public string CustomerFacingName { get; set; }
[Parameter]
public string AccountNumber { get; set; }
[Parameter]
public string EntityName { get; set; }
[Parameter]
public bool VisibleInMemberView { get; set; }
[Parameter]
public Func<Task> DownloadDocument { get; set; }
[Parameter]
public DateTime CreatedDate { get; set; }
[Parameter]
public int OnlineBankingRetentionMonths { get; set; }
[Parameter]
public int InternalRetentionMonths { get; set; }
[Parameter]
public string DocumentId { get; set; }
[Parameter]
public bool? ScanStatus { get; set; }
[Parameter]
public bool Scanned { get; set; }
private bool IsAllowedToDownload => Auth.PolicySatisfied(PolicyConstants.DocumentLookupDownload);
private string RetentionMessage { get; set; } = String.Empty;
private bool DateIsAfterInternalRetentionDate() => (DateTime.Now > CreatedDate.AddMonths(InternalRetentionMonths));
private bool DateIsAfterOnlineRetentionDate() => (DateTime.Now > CreatedDate.AddMonths(OnlineBankingRetentionMonths));
protected override void OnInitialized()
{
if (DateIsAfterInternalRetentionDate())
{
RetentionMessage = "Document is past the internal retention period and is not available for download.";
} else if (DateIsAfterOnlineRetentionDate())
{
RetentionMessage = "Document is past the online retention period.";
}
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) JSTooltips.Enable();
return base.OnAfterRenderAsync(firstRender);
}
}
@@ -0,0 +1,4 @@
.doc-type-disabled-warning {
background: rgba(255, 0, 0, .2);
color: red;
}
@@ -0,0 +1,82 @@
@using LCU.Library.DocumentType.Extensions
@using LCU.Library.DocumentType.Enums
<RowSubHeader>Document Authorizations</RowSubHeader>
<div class="d-flex">
<div class="col col-4">
<strong>
Primary:
</strong>
<ul>
@if(PrimaryOwnerHasAccess)
{
<li>
@PrimaryOwnerName
</li>
}
@if(!PrimaryOwnerHasAccess)
{
<li class="removed">
@PrimaryOwnerName
</li>
}
</ul>
</div>
<div class="col col-8">
<strong>
Additional:
</strong>
<ul>
@foreach(var access in Document.DocumentAccess.Where(x => x.AccessType == AccessType.Owner))
{
<li>
@access.EntityName
</li>
}
@foreach(var access in Document.DocumentAccess.Where(x => x.AccessType == AccessType.Additional))
{
<li>
<span class="oi oi-eye mr-2"></span>@access.EntityName
</li>
}
@if (EnumExtensions.IsDocumentTypeOf<AccountDocumentTypes>(Document.DocumentCode))
{
@foreach(var access in Document.DocumentAccess.Where(x => x.AccessType == AccessType.Removed))
{
<li class="removed">
@access.EntityName
</li>
}
}
</ul>
</div>
</div>
@code {
[Parameter]
public DocumentInformation Document { get; set; }
private string PrimaryOwnerName { get; set; }
private bool PrimaryOwnerHasAccess { get; set; }
protected override void OnInitialized()
{
UpdatePrimaryOwner();
Document.DocumentAccess.CollectionChanged += (_, _) =>
{
UpdatePrimaryOwner();
StateHasChanged();
};
}
private void UpdatePrimaryOwner()
{
var primaryOwner = Document.DocumentAccess.FirstOrDefault(x => x.AccessType == AccessType.PrimaryOwner);
if(primaryOwner != null)
{
PrimaryOwnerName = primaryOwner.EntityName;
PrimaryOwnerHasAccess = primaryOwner.HasAccess;
}
}
}
@@ -0,0 +1,8 @@
ul {
padding: 0;
list-style: none;
}
.removed {
text-decoration: line-through red;
}

Some files were not shown because too many files have changed in this diff Show More