MRB webassembly
This commit is contained in:
26
MesaFabApproval.Client/App.razor
Normal file
26
MesaFabApproval.Client/App.razor
Normal file
@ -0,0 +1,26 @@
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" >
|
||||
<NotAuthorized>
|
||||
@if (context.User.Identity?.IsAuthenticated != true) {
|
||||
<RedirectToLogin />
|
||||
} else {
|
||||
<p role="alert">You are not authorized to access this resource.</p>
|
||||
}
|
||||
</NotAuthorized>
|
||||
<Authorizing>
|
||||
<MudProgressCircular Color="Color.Tertiary" Indeterminate="true" />
|
||||
<div>Authorizing...</div>
|
||||
</Authorizing>
|
||||
</AuthorizeRouteView>
|
||||
<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>
|
||||
</CascadingAuthenticationState>
|
70
MesaFabApproval.Client/Layout/MainLayout.razor
Normal file
70
MesaFabApproval.Client/Layout/MainLayout.razor
Normal file
@ -0,0 +1,70 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@inject MesaFabApprovalAuthStateProvider authStateProvider
|
||||
@inject IConfiguration Configuration
|
||||
@inject IMemoryCache cache
|
||||
@inject NavigationManager navManager
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<div style="height: 100vh;">
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="1" Color="Color.Info">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
|
||||
<MudText Typo="Typo.h5" Class="ml-3">Mesa Fab Approval</MudText>
|
||||
@if (authStateProvider.CurrentUser is not null) {
|
||||
<MudSpacer />
|
||||
<MudText Typo="Typo.h6" Class="mr-3">@authStateProvider.CurrentUser.FirstName @authStateProvider.CurrentUser.LastName</MudText>
|
||||
<MudIconButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick=Logout
|
||||
Edge="Edge.End"
|
||||
Icon="@Icons.Material.Filled.Logout" />
|
||||
}
|
||||
</MudAppBar>
|
||||
<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
|
||||
<MudNavMenu Color="Color.Info" Bordered="true" Class="d-flex flex-column justify-center p-1 gap-1">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
Href="@Configuration["OldFabApprovalUrl"]"
|
||||
Target="_blank"
|
||||
StartIcon="@Icons.Material.Filled.Home">
|
||||
Return to Main Site
|
||||
</MudButton>
|
||||
<MudDivider Class="my-1" />
|
||||
@if (authStateProvider.CurrentUser is not null) {
|
||||
<MudNavGroup Title="Create New">
|
||||
<MudNavLink OnClick="@(() => GoTo("mrb/new"))">Create New MRB</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavLink OnClick="@(() => GoTo(""))" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
||||
<MudNavLink OnClick="@(() => GoTo("mrb/all"))" Icon="@Icons.Material.Filled.Ballot">MRB</MudNavLink>
|
||||
}
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
<div style="display: flex; flex-flow: column; height: 100%;">
|
||||
<MudMainContent Style="@($"background:{Colors.Grey.Lighten2}; flex-grow: 1;")">
|
||||
@Body
|
||||
</MudMainContent>
|
||||
</div>
|
||||
</MudLayout>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
bool _drawerOpen = true;
|
||||
|
||||
void DrawerToggle() {
|
||||
_drawerOpen = !_drawerOpen;
|
||||
}
|
||||
|
||||
void Logout() {
|
||||
authStateProvider.Logout();
|
||||
}
|
||||
|
||||
private void GoTo(string page) {
|
||||
DrawerToggle();
|
||||
cache.Set("redirectUrl", page);
|
||||
navManager.NavigateTo(page);
|
||||
}
|
||||
}
|
77
MesaFabApproval.Client/Layout/MainLayout.razor.css
Normal file
77
MesaFabApproval.Client/Layout/MainLayout.razor.css
Normal file
@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
26
MesaFabApproval.Client/MesaFabApproval.Client.csproj
Normal file
26
MesaFabApproval.Client/MesaFabApproval.Client.csproj
Normal file
@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageReference Include="MudBlazor" Version="6.20.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MesaFabApproval.Shared\MesaFabApproval.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
64
MesaFabApproval.Client/Pages/AuthenticatedRedirect.razor
Normal file
64
MesaFabApproval.Client/Pages/AuthenticatedRedirect.razor
Normal file
@ -0,0 +1,64 @@
|
||||
@page "/redirect"
|
||||
@attribute [AllowAnonymous]
|
||||
@inject MesaFabApprovalAuthStateProvider authStateProvider
|
||||
@inject IAuthenticationService authService
|
||||
@inject IUserService userService
|
||||
@inject ISnackbar snackbar
|
||||
@inject MesaFabApprovalAuthStateProvider authStateProvider
|
||||
@inject NavigationManager navigationManager
|
||||
|
||||
@code {
|
||||
private string? _jwt;
|
||||
private string? _refreshToken;
|
||||
private string? _redirectPath;
|
||||
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
try {
|
||||
Uri uri = navigationManager.ToAbsoluteUri(navigationManager.Uri);
|
||||
|
||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("jwt", out var jwt)) {
|
||||
_jwt = System.Net.WebUtility.UrlDecode(jwt);
|
||||
}
|
||||
|
||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("refreshToken", out var refreshToken)) {
|
||||
_refreshToken = System.Net.WebUtility.UrlDecode(refreshToken);
|
||||
}
|
||||
|
||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("redirectPath", out var redirectPath)) {
|
||||
_redirectPath = System.Net.WebUtility.UrlDecode(redirectPath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_jwt) && !string.IsNullOrWhiteSpace(_refreshToken)) {
|
||||
await authService.SetTokens(_jwt, _refreshToken);
|
||||
|
||||
ClaimsPrincipal principal = authService.GetClaimsPrincipalFromJwt(_jwt);
|
||||
|
||||
string loginId = userService.GetLoginIdFromClaimsPrincipal(principal);
|
||||
|
||||
await authService.SetLoginId(loginId);
|
||||
await authService.SetTokens(_jwt, _refreshToken);
|
||||
|
||||
User? user = await userService.GetUserByLoginId(loginId);
|
||||
await authService.SetCurrentUser(user);
|
||||
|
||||
await authStateProvider.StateHasChanged(principal);
|
||||
}
|
||||
|
||||
if (authStateProvider.CurrentUser is not null && !string.IsNullOrWhiteSpace(_redirectPath)) {
|
||||
navigationManager.NavigateTo(_redirectPath);
|
||||
} else {
|
||||
await authStateProvider.Logout();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_redirectPath)) {
|
||||
navigationManager.NavigateTo($"login/{_redirectPath}");
|
||||
} else {
|
||||
navigationManager.NavigateTo("login");
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add($"Redirect failed, because {ex.Message}", Severity.Error);
|
||||
navigationManager.NavigateTo("login");
|
||||
}
|
||||
}
|
||||
}
|
69
MesaFabApproval.Client/Pages/Components/Comments.razor
Normal file
69
MesaFabApproval.Client/Pages/Components/Comments.razor
Normal file
@ -0,0 +1,69 @@
|
||||
@inject ISnackbar snackbar
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudPaper Class="p-2">
|
||||
<MudForm @bind-Errors="@errors">
|
||||
<MudTextField T="string"
|
||||
Label="Comments"
|
||||
Required="true"
|
||||
RequiredError="You must provide a comment"
|
||||
@bind-Value="@comments"
|
||||
@bind-Text="@comments"
|
||||
Immediate="true"
|
||||
AutoGrow
|
||||
AutoFocus/>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
Class="m1-auto"
|
||||
OnClick=SubmitComments>
|
||||
@if (processing) {
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Processing</MudText>
|
||||
} else {
|
||||
<MudText>Ok</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Class="grey text-black m1-auto"
|
||||
OnClick=Cancel>
|
||||
Cancel
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string comments { get; set; } = "";
|
||||
|
||||
private string[] errors = { };
|
||||
|
||||
private bool processing = false;
|
||||
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
comments = string.Empty;
|
||||
}
|
||||
|
||||
private void SubmitComments() {
|
||||
processing = true;
|
||||
try {
|
||||
if (string.IsNullOrWhiteSpace(comments) || comments.Length < 5)
|
||||
throw new Exception("Comments must be at least five characters long");
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(comments));
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
processing = false;
|
||||
}
|
||||
|
||||
private void Cancel() {
|
||||
MudDialog.Cancel();
|
||||
}
|
||||
}
|
166
MesaFabApproval.Client/Pages/Components/MRBActionForm.razor
Normal file
166
MesaFabApproval.Client/Pages/Components/MRBActionForm.razor
Normal file
@ -0,0 +1,166 @@
|
||||
@inject IMRBService mrbService
|
||||
@inject ISnackbar snackbar
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudPaper Class="p-2">
|
||||
<MudForm @bind-Errors="@errors">
|
||||
<MudSelect T="string"
|
||||
Label="Action"
|
||||
Required="true"
|
||||
RequiredError="You must select an action!"
|
||||
@bind-Value="@mrbAction.Action"
|
||||
Text="@mrbAction.Action">
|
||||
<MudSelectItem Value="@("Block")" />
|
||||
<MudSelectItem Value="@("Convert")" />
|
||||
<MudSelectItem Value="@("Other")" />
|
||||
<MudSelectItem Value="@("Recall")" />
|
||||
<MudSelectItem Value="@("Scrap")" />
|
||||
<MudSelectItem Value="@("Unblock")" />
|
||||
<MudSelectItem Value="@("Waiver")" />
|
||||
</MudSelect>
|
||||
<MudTextField T="string"
|
||||
Label="Customer / Vendor"
|
||||
Required="true"
|
||||
RequiredError="Customer / Vendor required!"
|
||||
@bind-Value="@mrbAction.Customer"
|
||||
Text="@mrbAction.Customer" />
|
||||
<MudTextField T="int"
|
||||
Label="Qty"
|
||||
Required="true"
|
||||
RequiredError="You must supply a quantity!"
|
||||
@bind-Value=mrbAction.Quantity
|
||||
Text="@mrbAction.Quantity.ToString()" />
|
||||
<MudTextField T="string"
|
||||
Label="Part Number"
|
||||
Required="true"
|
||||
RequiredError="Part number required!"
|
||||
@bind-Value="@mrbAction.PartNumber"
|
||||
Text="@mrbAction.PartNumber" />
|
||||
<MudTextField T="string"
|
||||
Label="Batch Number / Lot Number"
|
||||
Required="true"
|
||||
RequiredError="Batch number / Lot number required!"
|
||||
@bind-Value="@mrbAction.LotNumber"
|
||||
Text="@mrbAction.LotNumber" />
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
Class="m1-auto"
|
||||
OnClick=SaveMRBAction>
|
||||
@if (processingSave) {
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Processing</MudText>
|
||||
} else {
|
||||
<MudText>Save</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
@if (mrbAction is not null && mrbAction.ActionID > 0) {
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
Disabled="@(mrbAction is null || (mrbAction is not null && mrbAction.ActionID <= 0))"
|
||||
Class="m1-auto"
|
||||
OnClick=DeleteMRBAction>
|
||||
@if (processingDelete) {
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Processing</MudText>
|
||||
} else {
|
||||
<MudText>Delete</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Class="grey text-black m1-auto"
|
||||
OnClick=Cancel>
|
||||
Cancel
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public MRBAction mrbAction { get; set; }
|
||||
|
||||
private bool isVisible { get; set; }
|
||||
private string[] errors = { };
|
||||
private bool processingSave = false;
|
||||
private bool processingDelete = false;
|
||||
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
isVisible = true;
|
||||
|
||||
if (mrbAction is null) {
|
||||
mrbAction = new() {
|
||||
Action = "",
|
||||
Customer = "",
|
||||
Quantity = 0,
|
||||
PartNumber = "",
|
||||
LotNumber = "",
|
||||
MRBNumber = 0
|
||||
};
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool FormIsValid() {
|
||||
bool actionIsValid = mrbAction.Action.Equals("Block") || mrbAction.Action.Equals("Convert") ||
|
||||
mrbAction.Action.Equals("Other") || mrbAction.Action.Equals("Recall") || mrbAction.Action.Equals("Scrap") ||
|
||||
mrbAction.Action.Equals("Unblock") || mrbAction.Action.Equals("Waiver");
|
||||
return actionIsValid && !string.IsNullOrWhiteSpace(mrbAction.Customer) &&
|
||||
!string.IsNullOrWhiteSpace(mrbAction.PartNumber) &&
|
||||
!string.IsNullOrWhiteSpace(mrbAction.LotNumber);
|
||||
}
|
||||
|
||||
private async void SaveMRBAction() {
|
||||
processingSave = true;
|
||||
try {
|
||||
if (!FormIsValid()) throw new Exception("You must complete the form before saving!");
|
||||
|
||||
if (mrbAction.MRBNumber > 0) {
|
||||
if (mrbAction.ActionID <= 0) {
|
||||
await mrbService.CreateMRBAction(mrbAction);
|
||||
snackbar.Add("MRB action created", Severity.Success);
|
||||
} else {
|
||||
await mrbService.UpdateMRBAction(mrbAction);
|
||||
snackbar.Add("MRB action updated", Severity.Success);
|
||||
}
|
||||
} else {
|
||||
snackbar.Add("MRB action saved", Severity.Success);
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(mrbAction));
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
processingSave = false;
|
||||
}
|
||||
|
||||
private async void DeleteMRBAction() {
|
||||
processingDelete = true;
|
||||
try {
|
||||
if (mrbAction is null) throw new Exception("MRB action cannot be null!");
|
||||
if (mrbAction.ActionID <= 0)
|
||||
throw new Exception("You cannot delete an action before creating it!");
|
||||
if (mrbAction.MRBNumber <= 0)
|
||||
throw new Exception("Invalid MRB number!");
|
||||
|
||||
await mrbService.DeleteMRBAction(mrbAction);
|
||||
snackbar.Add("MRB action successfully deleted", Severity.Success);
|
||||
|
||||
MudDialog.Close(DialogResult.Ok<MRBAction>(null));
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
processingDelete = false;
|
||||
}
|
||||
|
||||
private void Cancel() {
|
||||
MudDialog.Cancel();
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
@attribute [AllowAnonymous]
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized() {
|
||||
Navigation.NavigateTo("login");
|
||||
}
|
||||
}
|
77
MesaFabApproval.Client/Pages/Dashboard.razor
Normal file
77
MesaFabApproval.Client/Pages/Dashboard.razor
Normal file
@ -0,0 +1,77 @@
|
||||
@page "/"
|
||||
@page "/Dashboard"
|
||||
@inject MesaFabApprovalAuthStateProvider stateProvider
|
||||
@inject IApprovalService approvalService
|
||||
@inject IMemoryCache cache
|
||||
@inject NavigationManager navigationManager
|
||||
@inject ISnackbar snackbar
|
||||
|
||||
<PageTitle>Dashboard</PageTitle>
|
||||
|
||||
<MudPaper Class="p-2 m-2" MinWidth="100%">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center">Dashboard</MudText>
|
||||
</MudPaper>
|
||||
|
||||
@if (stateProvider.CurrentUser is not null && approvalList is not null) {
|
||||
<MudPaper Outlined="true"
|
||||
Class="p-2 m-2 d-flex flex-column justify-center">
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">My Active Approvals</MudText>
|
||||
<MudDivider DividerType="DividerType.Middle" Class="my-2" />
|
||||
<MudTable Items="@approvalList"
|
||||
Class="m-2"
|
||||
Striped
|
||||
SortLabel="Sort by">
|
||||
<HeaderContent>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Descending" SortBy="new Func<Approval,object>(x=>x.IssueID)">
|
||||
Issue ID
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Descending" SortBy="new Func<Approval,object>(x=>x.SubRoleCategoryItem)">
|
||||
Role
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Descending" SortBy="new Func<Approval,object>(x=>x.AssignedDate)">
|
||||
Assigned Date
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Descending" SortBy="new Func<Approval,object>(x=>x.Step)">
|
||||
Step
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Issue ID">
|
||||
<MudLink OnClick="@(() => GoTo($"/mrb/{context.IssueID}"))">@context.IssueID</MudLink>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Role">@context.SubRoleCategoryItem</MudTd>
|
||||
<MudTd DataLabel="Assigned Date">@DateTimeUtilities.GetDateAsStringMinDefault(context.AssignedDate)</MudTd>
|
||||
<MudTd DataLabel="Step">@context.Step</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager />
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@code {
|
||||
private IEnumerable<Approval> approvalList = new List<Approval>();
|
||||
|
||||
protected async override Task OnParametersSetAsync() {
|
||||
try {
|
||||
if (stateProvider.CurrentUser is not null)
|
||||
approvalList = (await approvalService.GetApprovalsForUserId(stateProvider.CurrentUser.UserID, true)).ToList();
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add($"Unable to fetch your outstanding approvals, because {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void GoTo(string page) {
|
||||
cache.Set("redirectUrl", page);
|
||||
navigationManager.NavigateTo(page);
|
||||
}
|
||||
}
|
84
MesaFabApproval.Client/Pages/Login.razor
Normal file
84
MesaFabApproval.Client/Pages/Login.razor
Normal file
@ -0,0 +1,84 @@
|
||||
@page "/login"
|
||||
@page "/login/{redirectUrl}"
|
||||
@page "/login/{redirectUrl}/{redirectUrlSub}"
|
||||
@attribute [AllowAnonymous]
|
||||
@inject MesaFabApprovalAuthStateProvider authStateProvider
|
||||
@inject NavigationManager navManager
|
||||
@inject ISnackbar snackbar
|
||||
|
||||
<MudPaper Class="p-2 m-2">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center">Login</MudText>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="p-2 m-2">
|
||||
<MudForm @bind-IsValid="@success" @bind-Errors="@errors">
|
||||
<MudTextField T="string"
|
||||
Label="Windows Username"
|
||||
Required="true"
|
||||
RequiredError="Username is required!"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value=username
|
||||
Class="m-1"
|
||||
Immediate="true"
|
||||
AutoFocus
|
||||
OnKeyDown=SubmitIfEnter />
|
||||
<MudTextField T="string"
|
||||
Label="Windows Password"
|
||||
Required="true"
|
||||
RequiredError="Password is required!"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value=password
|
||||
InputType="InputType.Password"
|
||||
Class="m-1"
|
||||
Immediate="true"
|
||||
OnKeyDown=SubmitIfEnter />
|
||||
<MudButton
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
Disabled="@(!success)"
|
||||
Class="m-1"
|
||||
OnClick=SubmitLogin >
|
||||
@if (processing) {
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Processing</MudText>
|
||||
} else {
|
||||
<MudText>Log In</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? redirectUrl { get; set; }
|
||||
[Parameter]
|
||||
public string? redirectUrlSub { get; set; }
|
||||
private bool success;
|
||||
private bool processing = false;
|
||||
private string[] errors = { };
|
||||
private string? username;
|
||||
private string? password;
|
||||
|
||||
private async Task SubmitLogin() {
|
||||
processing = true;
|
||||
if (string.IsNullOrWhiteSpace(username)) snackbar.Add("Username is required!", Severity.Error);
|
||||
else if (string.IsNullOrWhiteSpace(password)) snackbar.Add("Password is required!", Severity.Error);
|
||||
else {
|
||||
await authStateProvider.LoginAsync(username, password);
|
||||
if (!string.IsNullOrWhiteSpace(redirectUrl) && !string.IsNullOrWhiteSpace(redirectUrlSub)) {
|
||||
navManager.NavigateTo($"{redirectUrl}/{redirectUrlSub}");
|
||||
} else if (!string.IsNullOrWhiteSpace(redirectUrl)) {
|
||||
navManager.NavigateTo(redirectUrl);
|
||||
} else {
|
||||
navManager.NavigateTo("dashboard");
|
||||
}
|
||||
}
|
||||
processing = false;
|
||||
}
|
||||
|
||||
private async Task SubmitIfEnter(KeyboardEventArgs e) {
|
||||
if (e.Key == "Enter" && success) {
|
||||
SubmitLogin();
|
||||
}
|
||||
}
|
||||
}
|
113
MesaFabApproval.Client/Pages/MRBAll.razor
Normal file
113
MesaFabApproval.Client/Pages/MRBAll.razor
Normal file
@ -0,0 +1,113 @@
|
||||
@page "/mrb/all"
|
||||
@using System.Globalization
|
||||
@inject IMRBService mrbService
|
||||
@inject ISnackbar snackbar
|
||||
@inject IMemoryCache cache
|
||||
@inject NavigationManager navigationManager
|
||||
|
||||
<PageTitle>MRB</PageTitle>
|
||||
|
||||
<MudPaper Class="p-2 m-2">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center">MRB List</MudText>
|
||||
</MudPaper>
|
||||
|
||||
@if (allMrbs is not null && allMrbs.Count() > 0) {
|
||||
<MudTable Items="@allMrbs"
|
||||
Class="m-2"
|
||||
Striped="true"
|
||||
Filter="new Func<MRB,bool>(FilterFuncForTable)"
|
||||
SortLabel="Sort By"
|
||||
Hover="true">
|
||||
<ToolBarContent>
|
||||
<MudSpacer />
|
||||
<MudTextField @bind-Value="searchString"
|
||||
Placeholder="Search"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
IconSize="Size.Medium"
|
||||
Class="mt-0" />
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Descending" SortBy="new Func<MRB,object>(x=>x.MRBNumber)">
|
||||
MRB#
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRB,object>(x=>x.Title)">
|
||||
Title
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRB,object>(x=>x.OriginatorName)">
|
||||
Originator
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRB,object>(x=>x.SubmittedDate)">
|
||||
Submitted Date
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRB,object>(x=>x.CloseDate)">
|
||||
Closed Date
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="MRB#">
|
||||
<MudLink OnClick="@(() => GoTo($"/mrb/{context.MRBNumber}"))">@context.MRBNumber</MudLink>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Title">@context.Title</MudTd>
|
||||
<MudTd DataLabel="Originator">@context.OriginatorName</MudTd>
|
||||
<MudTd DataLabel="Submitted Date">@DateTimeUtilities.GetDateAsStringMinDefault(context.SubmittedDate)</MudTd>
|
||||
<MudTd DataLabel="Closed Date">@DateTimeUtilities.GetDateAsStringMaxDefault(context.CloseDate)</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager />
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
}
|
||||
|
||||
<MudOverlay @bind-Visible=inProcess DarkBackground="true" AutoClose="false">
|
||||
<MudProgressCircular Color="Color.Info" Size="Size.Large" Indeterminate="true" />
|
||||
</MudOverlay>
|
||||
|
||||
@code {
|
||||
private bool inProcess = false;
|
||||
private string searchString = "";
|
||||
private IEnumerable<MRB> allMrbs = new List<MRB>();
|
||||
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
inProcess = true;
|
||||
try {
|
||||
if (mrbService is null) {
|
||||
throw new Exception("MRB service not injected!");
|
||||
} else {
|
||||
allMrbs = await mrbService.GetAllMRBs();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
inProcess = false;
|
||||
}
|
||||
|
||||
private bool FilterFuncForTable(MRB mrb) => FilterFunc(mrb, searchString);
|
||||
|
||||
private bool FilterFunc(MRB mrb, string searchString) {
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
return true;
|
||||
if (mrb.Title.ToLower().Contains(searchString.ToLower()))
|
||||
return true;
|
||||
if (mrb.OriginatorName.ToLower().Contains(searchString.ToLower()))
|
||||
return true;
|
||||
if (mrb.MRBNumber.ToString().Contains(searchString))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void GoTo(string page) {
|
||||
cache.Set("redirectUrl", page);
|
||||
navigationManager.NavigateTo(page);
|
||||
}
|
||||
}
|
996
MesaFabApproval.Client/Pages/MRBSingle.razor
Normal file
996
MesaFabApproval.Client/Pages/MRBSingle.razor
Normal file
@ -0,0 +1,996 @@
|
||||
@page "/mrb/{mrbNumber}"
|
||||
@page "/mrb/new"
|
||||
@using System.Text
|
||||
@inject IMRBService mrbService
|
||||
@inject ISnackbar snackbar
|
||||
@inject IDialogService dialogService
|
||||
@inject NavigationManager navigationManager
|
||||
@inject MesaFabApprovalAuthStateProvider authStateProvider
|
||||
@inject IUserService userService
|
||||
@inject IMemoryCache cache
|
||||
@inject IConfiguration config
|
||||
@inject IApprovalService approvalService
|
||||
|
||||
@if (mrbNumber is not null) {
|
||||
<PageTitle>MRB @mrbNumber</PageTitle>
|
||||
|
||||
<MudPaper Class="p-2 m-2 d-flex flex-row justify-content-between">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Dark"
|
||||
OnClick="@ReturnToAllMrbs"
|
||||
Size="Size.Large" />
|
||||
<MudText Typo="Typo.h3" Align="Align.Center">MRB @mrbNumber</MudText>
|
||||
<MudPaper Height="100%" Width="0.1%" Square="true" />
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@if (mrb is not null) {
|
||||
<MudTimeline Class="mt-2 pt-2" TimelineOrientation="TimelineOrientation.Horizontal"
|
||||
TimelinePosition="TimelinePosition.Bottom">
|
||||
@for (int i = 0; i < MRB.Stages.Length; i++) {
|
||||
Color color;
|
||||
if (mrb.StageNo > i || mrb.StageNo == (MRB.Stages.Length - 1)) {
|
||||
color = Color.Success;
|
||||
} else if (mrb.StageNo == i) {
|
||||
color = Color.Info;
|
||||
} else {
|
||||
color = Color.Dark;
|
||||
}
|
||||
|
||||
string stageName = MRB.Stages[i];
|
||||
|
||||
<MudTimelineItem Color="@color" Variant="Variant.Filled">
|
||||
<MudText Align="Align.Center" Color="@color">@stageName</MudText>
|
||||
</MudTimelineItem>
|
||||
}
|
||||
</MudTimeline>
|
||||
|
||||
bool mrbIsSubmitted = mrb.SubmittedDate > DateTime.MinValue;
|
||||
bool mrbReadyToSubmit = mrbIsReadyToSubmit();
|
||||
bool userIsApprover = currentUserIsApprover();
|
||||
|
||||
if (!mrbIsSubmitted || (!mrbIsSubmitted && mrbReadyToSubmit) || (mrbIsSubmitted && userIsApprover))
|
||||
{
|
||||
<MudPaper Outlined="true"
|
||||
Class="p-2 m-2 d-flex flex-wrap gap-3 justify-content-center align-content-center"
|
||||
Elevation="10">
|
||||
@if (!mrbIsSubmitted)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick=SaveMRB>
|
||||
@if (saveInProcess)
|
||||
{
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Processing</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>Save</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
@if (!mrbIsSubmitted && mrbReadyToSubmit)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick=SubmitMRBForApproval>
|
||||
@if (submitInProcess)
|
||||
{
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Processing</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>Submit for Approval</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
@if (mrbIsSubmitted && userIsApprover)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick=ApproveMRB>
|
||||
Approve
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick=DenyMRB>
|
||||
Deny
|
||||
</MudButton>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudPaper Outlined="true"
|
||||
Class="p-2 m-2 d-flex flex-wrap gap-3 justify-content-center align-content-start"
|
||||
Elevation="10">
|
||||
<MudTextField @bind-Value=mrb.MRBNumber
|
||||
Text="@mrb.MRBNumber.ToString()"
|
||||
T="int"
|
||||
Disabled="true"
|
||||
Label="MRB#"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value=mrb.Title
|
||||
Text="@mrb.Title"
|
||||
Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
T="string"
|
||||
AutoGrow
|
||||
Variant="Variant.Outlined"
|
||||
Label="Title" />
|
||||
|
||||
<MudTextField Disabled="true"
|
||||
T="string"
|
||||
Value="@DateTimeUtilities.GetDateAsStringMinDefault(mrb.SubmittedDate)"
|
||||
Label="Submit Date"
|
||||
Variant="Variant.Outlined" />
|
||||
<MudTextField Disabled="true"
|
||||
Label="Approval Date"
|
||||
Variant="Variant.Outlined"
|
||||
Value="@DateTimeUtilities.GetDateAsStringMaxDefault(mrb.ApprovalDate)"
|
||||
T="string" />
|
||||
<MudTextField Disabled="true"
|
||||
Label="Closed Date"
|
||||
Variant="Variant.Outlined"
|
||||
Value="@DateTimeUtilities.GetDateAsStringMaxDefault(mrb.CloseDate)"
|
||||
T="string" />
|
||||
@if (mrb is not null && mrb.CancelDate < DateTime.MaxValue) {
|
||||
<MudTextField Disabled="true"
|
||||
Label="Canceled Date"
|
||||
Variant="Variant.Outlined"
|
||||
Value="@DateTimeUtilities.GetDateAsStringMaxDefault(mrb.CancelDate)"
|
||||
T="string" />
|
||||
}
|
||||
|
||||
<MudTextField Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.IssueDescription
|
||||
Text="@mrb.IssueDescription"
|
||||
AutoGrow
|
||||
Variant="Variant.Outlined"
|
||||
Label="Description" />
|
||||
<MudSelect T="string"
|
||||
Label="Originator"
|
||||
Variant="Variant.Outlined"
|
||||
AnchorOrigin="Origin.BottomCenter"
|
||||
Disabled=@(mrb.SubmittedDate > DateTime.MinValue)
|
||||
@bind-Value=mrb.OriginatorName
|
||||
Text="@mrb.OriginatorName">
|
||||
@foreach (User user in allActiveUsers) {
|
||||
<MudSelectItem T="string" Value="@($"{user.FirstName} {user.LastName}")" />
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect T="string"
|
||||
Label="Department"
|
||||
Variant="Variant.Outlined"
|
||||
AnchorOrigin="Origin.BottomCenter"
|
||||
Disabled=@(mrb.SubmittedDate > DateTime.MinValue)
|
||||
@bind-Value=mrb.Department
|
||||
Text="@mrb.Department">
|
||||
<MudSelectItem Value="@("Production")" />
|
||||
<MudSelectItem Value="@("Engineering")" />
|
||||
<MudSelectItem Value="@("Materials")" />
|
||||
<MudSelectItem Value="@("Facilities")" />
|
||||
<MudSelectItem Value="@("Maintenance")" />
|
||||
<MudSelectItem Value="@("Quality")" />
|
||||
</MudSelect>
|
||||
<MudSelect T="string"
|
||||
Label="Process"
|
||||
Variant="Variant.Outlined"
|
||||
AnchorOrigin="Origin.BottomCenter"
|
||||
Disabled=@(mrb.SubmittedDate > DateTime.MinValue)
|
||||
@bind-Value=mrb.Process
|
||||
Text="@mrb.Process">
|
||||
<MudSelectItem Value="@("Receiving")" />
|
||||
<MudSelectItem Value="@("Kitting")" />
|
||||
<MudSelectItem Value="@("Cleans")" />
|
||||
<MudSelectItem Value="@("Reactor")" />
|
||||
<MudSelectItem Value="@("Metrology")" />
|
||||
<MudSelectItem Value="@("Final QA")" />
|
||||
<MudSelectItem Value="@("Packaging")" />
|
||||
<MudSelectItem Value="@("Shipping")" />
|
||||
<MudSelectItem Value="@("N/A")" />
|
||||
</MudSelect>
|
||||
<MudTextField Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.NumberOfLotsAffected
|
||||
Text="@mrb.NumberOfLotsAffected.ToString()"
|
||||
Variant="Variant.Outlined"
|
||||
Label="Total Quantity" />
|
||||
<MudTextField Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.Val
|
||||
Text="@mrb.Val"
|
||||
Variant="Variant.Outlined"
|
||||
Label="Value" />
|
||||
<MudTextField Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.RMANo
|
||||
Text="@mrb.RMANo.ToString()"
|
||||
Variant="Variant.Outlined"
|
||||
Label="RMA#" />
|
||||
<MudPaper Outlined="true" Class="p-2">
|
||||
<MudCheckBox Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.CustomerImpacted
|
||||
Color="Color.Tertiary"
|
||||
Label="Customer Impacted?"
|
||||
LabelPosition="LabelPosition.Start" />
|
||||
</MudPaper>
|
||||
<MudTextField Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.PCRBNo
|
||||
Text="@mrb.PCRBNo.ToString()"
|
||||
Variant="Variant.Outlined"
|
||||
Label="PCRB#" />
|
||||
<MudPaper Outlined="true" Class="p-2">
|
||||
<MudCheckBox Disabled="@(mrb.SubmittedDate > DateTime.MinValue)"
|
||||
@bind-Value=mrb.SpecsImpacted
|
||||
Color="Color.Tertiary"
|
||||
Label="Specs Impacted?"
|
||||
LabelPosition="LabelPosition.Start" />
|
||||
</MudPaper>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@if (mrb is not null && mrb.MRBNumber > 0) {
|
||||
<MudPaper Outlined="true"
|
||||
Class="p-2 m-2 d-flex flex-column justify-center">
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">Supporting Documents</MudText>
|
||||
<MudDivider DividerType="DividerType.Middle" Class="my-2" />
|
||||
@if (!(mrb.SubmittedDate > DateTime.MinValue)) {
|
||||
<MudFileUpload T="IBrowserFile" OnFilesChanged="AddAttachments" Class="centered-upload">
|
||||
<ButtonTemplate>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
style="margin: auto;"
|
||||
StartIcon="@Icons.Material.Filled.AttachFile"
|
||||
for="@context.Id">
|
||||
Upload Document
|
||||
</MudButton>
|
||||
</ButtonTemplate>
|
||||
</MudFileUpload>
|
||||
}
|
||||
|
||||
@if (mrbAttachments is not null) {
|
||||
<MudTable Items="@mrbAttachments"
|
||||
Class="m-2"
|
||||
Striped="true"
|
||||
Filter="new Func<MRBAttachment, bool>(FilterFuncForMRBAttachmentTable)"
|
||||
SortLabel="Sort By"
|
||||
Hover="true">
|
||||
<ToolBarContent>
|
||||
<MudSpacer />
|
||||
<MudTextField @bind-Value="attachmentSearchString"
|
||||
Placeholder="Search"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
IconSize="Size.Medium"
|
||||
Class="mt-0" />
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAttachment, object>(x=>x.FileName)">
|
||||
Name
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Descending" SortBy="new Func<MRBAttachment,object>(x=>x.UploadDate)">
|
||||
Upload Date
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Name">
|
||||
<a href="@(@$"{config["FabApprovalApiBaseUrl"]}/mrb/attachmentFile?path={context.Path}&fileName={context.FileName}")"
|
||||
download="@(context.FileName)"
|
||||
target="_top">
|
||||
@context.FileName
|
||||
</a>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Upload Date">@context.UploadDate.ToString("yyyy-MM-dd HH:mm")</MudTd>
|
||||
@if (mrb is not null && !(mrb.SubmittedDate > DateTime.MinValue)) {
|
||||
<MudTd>
|
||||
<MudButton Color="Color.Secondary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="@((e) => DeleteAttachment(context))">
|
||||
@if (deleteActionInProcess) {
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Deleting</MudText>
|
||||
} else {
|
||||
<MudText>Delete</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
}
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
|
||||
<MudPaper Outlined="true"
|
||||
Class="p-2 m-2 d-flex flex-column justify-center">
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">Actions</MudText>
|
||||
<MudDivider DividerType="DividerType.Middle" Class="my-2" />
|
||||
@if (!(mrb.SubmittedDate > DateTime.MinValue)) {
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
Class="object-center flex-grow-0"
|
||||
Style="max-width: 200px; margin: auto;"
|
||||
OnClick="@((e) => CreateNewAction())">
|
||||
Create New Action
|
||||
</MudButton>
|
||||
}
|
||||
|
||||
@if (mrbActions is not null) {
|
||||
<MudTable Items="@mrbActions"
|
||||
Class="m-2"
|
||||
Striped="true"
|
||||
Filter="new Func<MRBAction, bool>(FilterFuncForMRBActionTable)"
|
||||
SortLabel="Sort By"
|
||||
Hover="true">
|
||||
<ToolBarContent>
|
||||
<MudSpacer />
|
||||
<MudTextField @bind-Value="actionSearchString"
|
||||
Placeholder="Search"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
IconSize="Size.Medium"
|
||||
Class="mt-0" />
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Ascending" SortBy="new Func<MRBAction,object>(x=>x.Action)">
|
||||
Action
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAction,object>(x=>x.Customer)">
|
||||
Customer
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAction,object>(x=>x.Quantity)">
|
||||
Qty
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAction,object>(x=>x.PartNumber)">
|
||||
Part Number
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAction,object>(x=>x.LotNumber)">
|
||||
Batch Number / Lot Number
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAction, object>(x=>x.AssignedDate)">
|
||||
Assigned Date
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel SortBy="new Func<MRBAction, object>(x=>x.CompletedDate)">
|
||||
Completed Date
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
<MudTh>
|
||||
<MudTableSortLabel InitialDirection="SortDirection.Ascending" SortBy="new Func<MRBAction,object>(x=>x.CompletedByUser?.GetFullName() ??
|
||||
x.CompletedByUserID.ToString())">
|
||||
Completed By
|
||||
</MudTableSortLabel>
|
||||
</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Action">@context.Action</MudTd>
|
||||
<MudTd DataLabel="Customer">@context.Customer</MudTd>
|
||||
<MudTd DataLabel="Qty">@context.Quantity</MudTd>
|
||||
<MudTd DataLabel="Part Number">@context.PartNumber</MudTd>
|
||||
<MudTd DataLabel="Batch Number / Lot Number">@context.LotNumber</MudTd>
|
||||
<MudTd DataLabel="Assigned Date">@DateTimeUtilities.GetDateAsStringMinDefault(context.AssignedDate)</MudTd>
|
||||
<MudTd DataLabel="Completed Date">@DateTimeUtilities.GetDateAsStringMaxDefault(context.CompletedDate)</MudTd>
|
||||
<MudTd DataLabel="Completed By">@context.CompletedByUser?.GetFullName()</MudTd>
|
||||
@if (mrb is not null && !(mrb.SubmittedDate > DateTime.MinValue)) {
|
||||
<MudTd>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick="@((e) => EditAction(context))">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Secondary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="@((e) => DeleteAction(context))">
|
||||
@if (deleteActionInProcess) {
|
||||
<MudProgressCircular Class="m-1" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>Deleting</MudText>
|
||||
} else {
|
||||
<MudText>Delete</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
}
|
||||
@if (currentUser is not null && taskApprovals.Where(t => t.UserID == currentUser.UserID && t.TaskID == context.ActionID).ToList().Count() > 0 &&
|
||||
context.CompletedDate >= DateTime.MaxValue) {
|
||||
<MudTd>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
OnClick="@((e) => CompleteAction(context))">
|
||||
Mark Complete
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
}
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager />
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Outlined="true"
|
||||
Class="p-2 m-2 d-flex flex-column justify-center">
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">Approvals</MudText>
|
||||
|
||||
@if (nonTaskApprovals is not null) {
|
||||
<MudTable Items="@nonTaskApprovals"
|
||||
Class="m-2"
|
||||
Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>Role</MudTh>
|
||||
<MudTh>Approver Name</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh>Disposition Date</MudTh>
|
||||
<MudTh>Comments</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Role">@context.SubRoleCategoryItem</MudTd>
|
||||
<MudTd DataLabel="Approver Name">@context.User?.GetFullName()</MudTd>
|
||||
<MudTd DataLabel="Status">@context.StatusMessage</MudTd>
|
||||
<MudTd DataLabel="Disposition Date">@DateTimeUtilities.GetDateAsStringMaxDefault(context.CompletedDate)</MudTd>
|
||||
<MudTd DataLabel="Comments">@context.Comments</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudOverlay Visible=processing DarkBackground="true" AutoClose="false">
|
||||
<MudProgressCircular Color="Color.Info" Size="Size.Large" Indeterminate="true" />
|
||||
</MudOverlay>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? mrbNumber { get; set; }
|
||||
private int mrbNumberInt = 0;
|
||||
private MRB? mrb { get; set; }
|
||||
private User? currentUser = null;
|
||||
private int currentUserId = 0;
|
||||
private IEnumerable<User> allActiveUsers = new List<User>();
|
||||
private IEnumerable<MRBAction> mrbActions = new List<MRBAction>();
|
||||
private IEnumerable<MRBAttachment> mrbAttachments = new List<MRBAttachment>();
|
||||
private IEnumerable<Approval> mrbApprovals = new List<Approval>();
|
||||
private IEnumerable<Approval> nonTaskApprovals = new List<Approval>();
|
||||
private IEnumerable<Approval> taskApprovals = new List<Approval>();
|
||||
private bool processing = false;
|
||||
private bool saveInProcess = false;
|
||||
private bool submitInProcess = false;
|
||||
private bool approvalInProcess = false;
|
||||
private bool taskApprovalInProcess = false;
|
||||
private bool denialInProcess = false;
|
||||
private bool deleteActionInProcess = false;
|
||||
private bool deleteAttachmentInProcess = false;
|
||||
private string actionSearchString = "";
|
||||
private string attachmentSearchString = "";
|
||||
private string currentUrl = "";
|
||||
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
processing = true;
|
||||
try {
|
||||
if (allActiveUsers is null) allActiveUsers = await userService.GetAllActiveUsers();
|
||||
if (authStateProvider is not null) currentUser = authStateProvider.CurrentUser;
|
||||
if (navigationManager is not null) currentUrl = navigationManager.Uri;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mrbNumber) && Int32.TryParse(mrbNumber, out mrbNumberInt)) {
|
||||
mrb = await mrbService.GetMRBById(mrbNumberInt);
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(mrbNumberInt, false);
|
||||
mrbAttachments = await mrbService.GetAllAttachmentsForMRB(mrbNumberInt, false);
|
||||
|
||||
mrbApprovals = await approvalService.GetApprovalsForIssueId(mrbNumberInt, false);
|
||||
|
||||
nonTaskApprovals = mrbApprovals.Where(a => a.Step < 3).ToList();
|
||||
taskApprovals = mrbApprovals.Where(a => a.Step >= 3).ToList();
|
||||
} else {
|
||||
mrbNumberInt = 0;
|
||||
mrbNumber = "";
|
||||
mrb = new() { Status = "Draft", StageNo = 0 };
|
||||
mrbActions = new List<MRBAction>();
|
||||
mrbAttachments = new List<MRBAttachment>();
|
||||
mrbApprovals = new List<Approval>();
|
||||
nonTaskApprovals = new List<Approval>();
|
||||
taskApprovals = new List<Approval>();
|
||||
if (authStateProvider is not null && authStateProvider.CurrentUser is not null) {
|
||||
mrb.OriginatorID = authStateProvider.CurrentUser.UserID;
|
||||
mrb.OriginatorName = $"{authStateProvider.CurrentUser.FirstName} {authStateProvider.CurrentUser.LastName}";
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
processing = false;
|
||||
}
|
||||
|
||||
private void ReturnToAllMrbs() {
|
||||
cache.Set("redirectUrl", $"mrb/all");
|
||||
navigationManager.NavigateTo("mrb/all");
|
||||
}
|
||||
|
||||
private async void SaveMRB() {
|
||||
try {
|
||||
saveInProcess = true;
|
||||
MRB initialMrb = new MRB() {
|
||||
MRBNumber = mrb.MRBNumber,
|
||||
Status = mrb.Status,
|
||||
StageNo = 0
|
||||
};
|
||||
if (mrb is not null) {
|
||||
User? originator = allActiveUsers.Where(u => $"{u.FirstName} {u.LastName}".Equals(mrb.OriginatorName)).FirstOrDefault();
|
||||
if (originator is not null) mrb.OriginatorID = originator.UserID;
|
||||
|
||||
if (mrb.MRBNumber <= 0) {
|
||||
await mrbService.CreateNewMRB(mrb);
|
||||
mrb = await mrbService.GetMRBByTitle(mrb.Title, true);
|
||||
cache.Set("redirectUrl", $"mrb/{mrb.MRBNumber}");
|
||||
} else {
|
||||
await mrbService.UpdateMRB(mrb);
|
||||
}
|
||||
|
||||
mrbNumber = mrb.MRBNumber.ToString();
|
||||
mrbNumberInt = mrb.MRBNumber;
|
||||
|
||||
foreach (MRBAction action in mrbActions) {
|
||||
if (action is not null) {
|
||||
action.MRBNumber = mrbNumberInt;
|
||||
if (action.ActionID > 0) {
|
||||
await mrbService.UpdateMRBAction(action);
|
||||
} else {
|
||||
await mrbService.CreateMRBAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(mrbNumberInt, true);
|
||||
}
|
||||
saveInProcess = false;
|
||||
StateHasChanged();
|
||||
snackbar.Add($"MRB {mrb.MRBNumber} successfully saved", Severity.Success);
|
||||
|
||||
if (initialMrb.MRBNumber <= 0)
|
||||
navigationManager.NavigateTo($"mrb/{mrb.MRBNumber}");
|
||||
} catch (Exception ex) {
|
||||
saveInProcess = false;
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
saveInProcess = false;
|
||||
}
|
||||
|
||||
private async void SubmitMRBForApproval() {
|
||||
submitInProcess = true;
|
||||
try {
|
||||
if (mrb is null) throw new Exception("MRB cannot be null");
|
||||
|
||||
User? originator = allActiveUsers.Where(u => $"{u.FirstName} {u.LastName}".Equals(mrb.OriginatorName)).FirstOrDefault();
|
||||
if (originator is not null) mrb.OriginatorID = originator.UserID;
|
||||
|
||||
if (mrb.StageNo == 0) {
|
||||
mrb.StageNo++;
|
||||
mrb.SubmittedDate = DateTime.Now;
|
||||
await mrbService.UpdateMRB(mrb);
|
||||
}
|
||||
|
||||
foreach (MRBAction action in mrbActions) {
|
||||
if (action is not null) {
|
||||
action.MRBNumber = mrb.MRBNumber;
|
||||
if (action.ActionID > 0) {
|
||||
await mrbService.UpdateMRBAction(action);
|
||||
} else {
|
||||
await mrbService.CreateMRBAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(mrbNumberInt, true);
|
||||
|
||||
await mrbService.SubmitForApproval(mrb);
|
||||
|
||||
await mrbService.NotifyNewApprovals(mrb);
|
||||
|
||||
mrbApprovals = await approvalService.GetApprovalsForIssueId(mrb.MRBNumber, true);
|
||||
|
||||
nonTaskApprovals = mrbApprovals.Where(a => a.Step < 3).ToList();
|
||||
taskApprovals = mrbApprovals.Where(a => a.Step >= 3).ToList();
|
||||
|
||||
submitInProcess = false;
|
||||
|
||||
snackbar.Add("MRB submitted for approval", Severity.Success);
|
||||
} catch (Exception ex) {
|
||||
submitInProcess = false;
|
||||
snackbar.Add($"Unable to submit MRB for approval, because {ex.Message}", Severity.Error);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async void ApproveMRB() {
|
||||
approvalInProcess = true;
|
||||
try {
|
||||
if (mrbApprovals is null || mrbApprovals.Count() <= 0)
|
||||
throw new Exception("there are no approvals to approval");
|
||||
if (authStateProvider.CurrentUser is null) {
|
||||
navigationManager.NavigateTo("login");
|
||||
return;
|
||||
}
|
||||
if (mrb is null) throw new Exception("MRB is null");
|
||||
|
||||
string? comments = "";
|
||||
|
||||
DialogParameters<Comments> parameters = new DialogParameters<Comments> { { x => x.comments, comments } };
|
||||
var dialog = dialogService.Show<Comments>($"Approval Comments", parameters);
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
comments = result.Data.ToString();
|
||||
|
||||
if (result.Canceled) throw new Exception("you must provide your approval comments");
|
||||
|
||||
IEnumerable<Approval> approvals = mrbApprovals.Where(a => a.UserID == authStateProvider.CurrentUser.UserID &&
|
||||
!(a.CompletedDate < DateTime.MaxValue) &&
|
||||
a.Step == mrb.StageNo);
|
||||
|
||||
foreach (Approval approval in approvals) {
|
||||
approval.CompletedDate = DateTime.Now;
|
||||
approval.Comments = comments is null ? "" : comments;
|
||||
approval.ItemStatus = 1;
|
||||
await approvalService.Approve(approval);
|
||||
}
|
||||
|
||||
IEnumerable<Approval> remainingApprovalsInKind = approvals.Where(a => a.Step == mrb.StageNo &&
|
||||
a.UserID != authStateProvider.CurrentUser.UserID &&
|
||||
!(a.CompletedDate < DateTime.MaxValue));
|
||||
|
||||
if (remainingApprovalsInKind is null || remainingApprovalsInKind.Count() <= 0) {
|
||||
mrb.StageNo++;
|
||||
if (mrb.StageNo == 3) mrb.ApprovalDate = DateTime.Now;
|
||||
await mrbService.UpdateMRB(mrb);
|
||||
|
||||
if (mrb.StageNo < 3)
|
||||
SubmitMRBForApproval();
|
||||
|
||||
if (mrb.StageNo == 3) {
|
||||
GenerateActionTasks();
|
||||
|
||||
string body = $"Your MRB ({mrb.MRBNumber}) has been approved.";
|
||||
|
||||
MRBNotification notification = new() {
|
||||
MRB = mrb,
|
||||
Message = body
|
||||
};
|
||||
|
||||
await mrbService.NotifyOriginator(notification);
|
||||
}
|
||||
}
|
||||
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(mrb.MRBNumber, true);
|
||||
|
||||
mrbApprovals = await approvalService.GetApprovalsForIssueId(mrb.MRBNumber, true);
|
||||
taskApprovals = mrbApprovals.Where(a => a.Step >= 3).ToList();
|
||||
nonTaskApprovals = mrbApprovals.Where(a => a.Step < 3).ToList();
|
||||
|
||||
approvalInProcess = false;
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
snackbar.Add("Successfully approved", Severity.Success);
|
||||
} catch (Exception ex) {
|
||||
approvalInProcess = false;
|
||||
|
||||
snackbar.Add($"Unable to approve, because {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DenyMRB() {
|
||||
denialInProcess = true;
|
||||
try {
|
||||
if (mrbApprovals is null || mrbApprovals.Count() <= 0)
|
||||
throw new Exception("there are no approvals to deny");
|
||||
if (authStateProvider.CurrentUser is null) {
|
||||
navigationManager.NavigateTo("login");
|
||||
return;
|
||||
}
|
||||
if (mrb is null) throw new Exception("MRB is null");
|
||||
|
||||
string? comments = "";
|
||||
|
||||
DialogParameters<Comments> parameters = new DialogParameters<Comments> { { x => x.comments, comments } };
|
||||
var dialog = dialogService.Show<Comments>($"Denial Comments", parameters);
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
comments = result.Data.ToString();
|
||||
|
||||
if (result.Canceled) throw new Exception("you must provide your denial comments");
|
||||
|
||||
IEnumerable<Approval> approvals = mrbApprovals.Where(a => a.UserID == authStateProvider.CurrentUser.UserID &&
|
||||
!(a.CompletedDate < DateTime.MaxValue) &&
|
||||
a.Step == mrb.StageNo);
|
||||
|
||||
foreach (Approval approval in approvals) {
|
||||
approval.CompletedDate = DateTime.Now;
|
||||
approval.Comments = comments is null ? "" : comments;
|
||||
approval.ItemStatus = -1;
|
||||
await approvalService.Deny(approval);
|
||||
}
|
||||
|
||||
if (mrb.StageNo < 2) {
|
||||
mrb.StageNo = 0;
|
||||
mrb.SubmittedDate = DateTime.MinValue;
|
||||
} else {
|
||||
IEnumerable<Approval> remainingApprovalsInKind = approvals.Where(a => a.Step == mrb.StageNo &&
|
||||
a.UserID != authStateProvider.CurrentUser.UserID &&
|
||||
!(a.CompletedDate < DateTime.MaxValue));
|
||||
|
||||
if (remainingApprovalsInKind is null || remainingApprovalsInKind.Count() <= 0) {
|
||||
mrb.CloseDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
await mrbService.UpdateMRB(mrb);
|
||||
|
||||
mrbApprovals = await approvalService.GetApprovalsForIssueId(mrb.MRBNumber, true);
|
||||
nonTaskApprovals = mrbApprovals.Where(a => a.Step < 3).ToList();
|
||||
|
||||
string body = $"Your MRB ({mrb.MRBNumber}) has been denied.";
|
||||
|
||||
MRBNotification notification = new() {
|
||||
MRB = mrb,
|
||||
Message = body
|
||||
};
|
||||
|
||||
await mrbService.NotifyOriginator(notification);
|
||||
|
||||
denialInProcess = false;
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
snackbar.Add("Successfully approved", Severity.Success);
|
||||
} catch (Exception ex) {
|
||||
denialInProcess = false;
|
||||
|
||||
snackbar.Add($"Unable to approve, because {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void GenerateActionTasks() {
|
||||
try {
|
||||
if (mrb is null) throw new Exception("MRB cannot be null");
|
||||
|
||||
foreach (MRBAction action in mrbActions) {
|
||||
action.AssignedDate = DateTime.Now;
|
||||
await mrbService.UpdateMRBAction(action);
|
||||
|
||||
await mrbService.GenerateActionTasks(mrb, action);
|
||||
}
|
||||
|
||||
await mrbService.NotifyNewApprovals(mrb);
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add($"Unable to generate action tasks, because {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void CompleteAction(MRBAction action) {
|
||||
try {
|
||||
if (action is null) throw new Exception("MRB action cannot be null");
|
||||
if (authStateProvider.CurrentUser is null)
|
||||
throw new Exception("you must be logged in to complete this action");
|
||||
if (mrb is null) throw new Exception("MRB cannot be null");
|
||||
|
||||
action.CompletedDate = DateTime.Now;
|
||||
action.CompletedByUserID = authStateProvider.CurrentUser.UserID;
|
||||
action.CompletedByUser = authStateProvider.CurrentUser;
|
||||
|
||||
await mrbService.UpdateMRBAction(action);
|
||||
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(action.MRBNumber, true);
|
||||
|
||||
string role = "";
|
||||
foreach (Approval approval in taskApprovals) {
|
||||
bool approved = false;
|
||||
if (approval.UserID == action.CompletedByUserID && approval.ItemStatus == 0 && approval.TaskID == action.ActionID) {
|
||||
approved = true;
|
||||
role = approval.SubRoleCategoryItem;
|
||||
await approvalService.Approve(approval);
|
||||
}
|
||||
|
||||
if (!approved && approval.SubRoleCategoryItem.Equals(role))
|
||||
await approvalService.Approve(approval);
|
||||
}
|
||||
|
||||
mrbApprovals = await approvalService.GetApprovalsForIssueId(mrb.MRBNumber, true);
|
||||
taskApprovals = mrbApprovals.Where(a => a.Step >= 3).ToList();
|
||||
|
||||
int outStandingTaskCount = taskApprovals.Where(a => a.CompletedDate >= DateTime.MaxValue).Count();
|
||||
|
||||
if (outStandingTaskCount == 0) {
|
||||
mrb.StageNo++;
|
||||
mrb.CloseDate = DateTime.Now;
|
||||
await mrbService.UpdateMRB(mrb);
|
||||
|
||||
string body = $"Your MRB ({mrb.MRBNumber}) is complete.";
|
||||
|
||||
MRBNotification notification = new() {
|
||||
MRB = mrb,
|
||||
Message = body
|
||||
};
|
||||
|
||||
await mrbService.NotifyOriginator(notification);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add($"Unable to mark action complete, because {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private bool mrbIsReadyToSubmit() {
|
||||
return mrb is not null && !(mrb.SubmittedDate > DateTime.MinValue) && mrb.MRBNumber > 0 && mrb.OriginatorID > 0 && !mrb.OriginatorName.Equals("") &&
|
||||
!mrb.Title.Equals("") && !mrb.IssueDescription.Equals("") && !mrb.Department.Equals("") && !mrb.Process.Equals("");
|
||||
}
|
||||
|
||||
private bool currentUserIsApprover() {
|
||||
if (mrbApprovals is null || authStateProvider is null) return false;
|
||||
if (authStateProvider.CurrentUser is null) return false;
|
||||
|
||||
IEnumerable<Approval> approvalsForCurrentUser = mrbApprovals.Where(a => mrb is not null &&
|
||||
mrb.StageNo < 3 &&
|
||||
a.UserID == authStateProvider.CurrentUser.UserID &&
|
||||
a.ItemStatus == 0);
|
||||
|
||||
if (approvalsForCurrentUser is not null && approvalsForCurrentUser.Count() > 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async void CreateNewAction() {
|
||||
try {
|
||||
MRBAction mrbAction = new() {
|
||||
Action = "",
|
||||
Customer = "",
|
||||
LotNumber = "",
|
||||
PartNumber = "",
|
||||
MRBNumber = mrbNumberInt,
|
||||
Quantity = 0
|
||||
};
|
||||
|
||||
DialogParameters<MRBActionForm> parameters = new DialogParameters<MRBActionForm> { { x => x.mrbAction, mrbAction } };
|
||||
var dialog = dialogService.Show<MRBActionForm>($"New MRB Action", parameters);
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled) {
|
||||
if (mrbNumberInt > 0) {
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(mrbNumberInt, true);
|
||||
} else {
|
||||
List<MRBAction> actionList = mrbActions.ToList();
|
||||
actionList.Add(mrbAction);
|
||||
mrbActions = actionList;
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void EditAction(MRBAction mrbAction) {
|
||||
try {
|
||||
if (mrbAction is null)
|
||||
throw new ArgumentNullException("Action cannot be null");
|
||||
|
||||
var parameters = new DialogParameters<MRBActionForm> { { x => x.mrbAction, mrbAction } };
|
||||
var dialog = dialogService.Show<MRBActionForm>($"MRB Action {mrbAction.ActionID}", parameters);
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled) {
|
||||
if (mrbNumberInt > 0) {
|
||||
mrbActions = await mrbService.GetMRBActionsForMRB(mrbNumberInt, true);
|
||||
} else {
|
||||
List<MRBAction> actionList = mrbActions.ToList();
|
||||
actionList.Add(mrbAction);
|
||||
mrbActions = actionList;
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DeleteAction(MRBAction mrbAction) {
|
||||
deleteActionInProcess = true;
|
||||
try {
|
||||
if (mrbAction is null)
|
||||
throw new ArgumentNullException("Action cannot be null");
|
||||
|
||||
await mrbService.DeleteMRBAction(mrbAction);
|
||||
|
||||
List<MRBAction> mrbActionList = mrbActions.ToList();
|
||||
mrbActionList.RemoveAll(x => x.ActionID == mrbAction.ActionID);
|
||||
mrbActions = mrbActionList;
|
||||
|
||||
snackbar.Add("Action successfully deleted", Severity.Success);
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
deleteActionInProcess = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool FilterFuncForMRBActionTable(MRBAction action) => MRBActionFilterFunc(action, actionSearchString);
|
||||
|
||||
private bool MRBActionFilterFunc(MRBAction action, string searchString) {
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
return true;
|
||||
|
||||
string search = searchString.ToLower();
|
||||
if (action.Customer.ToLower().Contains(search))
|
||||
return true;
|
||||
if (action.Action.ToLower().Contains(search))
|
||||
return true;
|
||||
if (action.PartNumber.ToLower().Contains(search))
|
||||
return true;
|
||||
if (action.LotNumber.ToLower().Contains(search))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task AddAttachments(InputFileChangeEventArgs args) {
|
||||
List<IBrowserFile> attachments = new() { args.File };
|
||||
|
||||
if (authStateProvider.CurrentUser is not null) {
|
||||
await mrbService.UploadAttachments(attachments, mrbNumberInt);
|
||||
|
||||
mrbAttachments = await mrbService.GetAllAttachmentsForMRB(mrbNumberInt, true);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async void DeleteAttachment(MRBAttachment mrbAttachment) {
|
||||
deleteAttachmentInProcess = true;
|
||||
try {
|
||||
if (mrbAttachment is null)
|
||||
throw new ArgumentNullException("Attachment cannot be null");
|
||||
|
||||
await mrbService.DeleteAttachment(mrbAttachment);
|
||||
|
||||
List<MRBAttachment> mrbAttachmentList = mrbAttachments.ToList();
|
||||
mrbAttachmentList.RemoveAll(x => x.AttachmentID == mrbAttachment.AttachmentID);
|
||||
mrbAttachments = mrbAttachmentList;
|
||||
|
||||
snackbar.Add("Attachment successfully deleted", Severity.Success);
|
||||
} catch (Exception ex) {
|
||||
snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
deleteAttachmentInProcess = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool FilterFuncForMRBAttachmentTable(MRBAttachment attachment) => MRBAttachmentFilterFunc(attachment, attachmentSearchString);
|
||||
|
||||
private bool MRBAttachmentFilterFunc(MRBAttachment attachment, string searchString) {
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
return true;
|
||||
|
||||
string search = searchString.ToLower();
|
||||
if (attachment.FileName.ToLower().Contains(search))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
56
MesaFabApproval.Client/Program.cs
Normal file
56
MesaFabApproval.Client/Program.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using MesaFabApproval.Client;
|
||||
using MesaFabApproval.Client.Utilities;
|
||||
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
using MudBlazor.Services;
|
||||
using MesaFabApproval.Client.Services;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using System.Net.Http.Headers;
|
||||
using MudBlazor;
|
||||
using Blazored.SessionStorage;
|
||||
|
||||
WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
string _apiBaseUrl = builder.Configuration["FabApprovalApiBaseUrl"] ??
|
||||
throw new NullReferenceException("FabApprovalApiBaseUrl not found in config");
|
||||
|
||||
builder.Services.AddBlazoredSessionStorage();
|
||||
|
||||
builder.Services.AddTransient<ApiHttpClientHandler>();
|
||||
|
||||
builder.Services
|
||||
.AddHttpClient("API", client => {
|
||||
client.BaseAddress = new Uri(_apiBaseUrl);
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
})
|
||||
.AddHttpMessageHandler<ApiHttpClientHandler>();
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.AddMudServices(config => {
|
||||
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter;
|
||||
config.SnackbarConfiguration.PreventDuplicates = true;
|
||||
config.SnackbarConfiguration.MaxDisplayedSnackbars = 5;
|
||||
config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
|
||||
config.SnackbarConfiguration.ShowCloseIcon = true;
|
||||
config.SnackbarConfiguration.VisibleStateDuration = 5000;
|
||||
config.SnackbarConfiguration.HideTransitionDuration = 500;
|
||||
config.SnackbarConfiguration.ShowTransitionDuration = 500;
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<IMRBService, MRBService>();
|
||||
builder.Services.AddScoped<IApprovalService, ApprovalService>();
|
||||
builder.Services.AddScoped<MesaFabApprovalAuthStateProvider>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
|
||||
sp.GetRequiredService<MesaFabApprovalAuthStateProvider>());
|
||||
|
||||
builder.Services.AddAuthorizationCore();
|
||||
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
await builder.Build().RunAsync();
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<DeleteExistingFiles>true</DeleteExistingFiles>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<PublishProvider>FileSystem</PublishProvider>
|
||||
<PublishUrl>bin\Release\net8.0\browser-wasm\publish\</PublishUrl>
|
||||
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
|
||||
<ProjectGuid>34d52f44-a81f-4247-8180-16e204824a07</ProjectGuid>
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
</Project>
|
303
MesaFabApproval.Client/Services/ApprovalService.cs
Normal file
303
MesaFabApproval.Client/Services/ApprovalService.cs
Normal file
@ -0,0 +1,303 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using MesaFabApproval.Shared.Models;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace MesaFabApproval.Client.Services;
|
||||
|
||||
public interface IApprovalService {
|
||||
Task<int> GetRoleIdForRoleName(string roleName);
|
||||
Task<IEnumerable<SubRole>> GetSubRolesForSubRoleName(string subRoleName, int roleId);
|
||||
Task<IEnumerable<User>> GetApprovalGroupMembers(int subRoleId);
|
||||
Task CreateApproval(Approval approval);
|
||||
Task UpdateApproval(Approval approval);
|
||||
Task Approve(Approval approval);
|
||||
Task Deny(Approval approval);
|
||||
Task<IEnumerable<Approval>> GetApprovalsForIssueId(int issueId, bool bypassCache);
|
||||
Task<IEnumerable<Approval>> GetApprovalsForUserId(int userId, bool bypassCache);
|
||||
}
|
||||
|
||||
public class ApprovalService : IApprovalService {
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public ApprovalService(IMemoryCache cache, IHttpClientFactory httpClientFactory) {
|
||||
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
|
||||
_httpClientFactory = httpClientFactory ??
|
||||
throw new ArgumentNullException("IHttpClientFactory not injected");
|
||||
}
|
||||
|
||||
public async Task CreateApproval(Approval approval) {
|
||||
if (approval is null) throw new ArgumentNullException("approval cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, "approval");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(approval),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Unable to create approval, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
await GetApprovalsForIssueId(approval.IssueID, true);
|
||||
await GetApprovalsForUserId(approval.UserID, true);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Approval>> GetApprovalsForIssueId(int issueId, bool bypassCache) {
|
||||
if (issueId <= 0) throw new ArgumentException($"{issueId} is not a valid issue ID");
|
||||
|
||||
IEnumerable<Approval>? approvals = null;
|
||||
|
||||
if (!bypassCache)
|
||||
approvals = _cache.Get<IEnumerable<Approval>>($"approvals{issueId}");
|
||||
|
||||
if (approvals is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"approval/issue?issueId={issueId}&bypassCache={bypassCache}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
approvals = JsonSerializer.Deserialize<IEnumerable<Approval>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse approvals from API response");
|
||||
|
||||
_cache.Set($"approvals{issueId}", approvals, DateTimeOffset.Now.AddMinutes(15));
|
||||
} else {
|
||||
throw new Exception($"Unable to get approvals, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Approval approval in approvals) {
|
||||
if (approval.ItemStatus < 0)
|
||||
approval.StatusMessage = "Denied";
|
||||
if (approval.ItemStatus == 0)
|
||||
approval.StatusMessage = "Assigned";
|
||||
if (approval.ItemStatus > 0)
|
||||
approval.StatusMessage = "Approved";
|
||||
}
|
||||
|
||||
return approvals;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Approval>> GetApprovalsForUserId(int userId, bool bypassCache) {
|
||||
if (userId <= 0) throw new ArgumentException($"{userId} is not a valid user ID");
|
||||
|
||||
IEnumerable<Approval>? approvals = null;
|
||||
|
||||
if (!bypassCache) approvals = _cache.Get<IEnumerable<Approval>>($"approvals{userId}");
|
||||
|
||||
if (approvals is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"approval/user?userId={userId}&bypassCache={bypassCache}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
approvals = JsonSerializer.Deserialize<IEnumerable<Approval>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse approvals from API response");
|
||||
|
||||
_cache.Set($"approvals{userId}", approvals, DateTimeOffset.Now.AddMinutes(15));
|
||||
} else {
|
||||
throw new Exception($"Unable to get approvals, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Approval approval in approvals) {
|
||||
if (approval.ItemStatus < 0)
|
||||
approval.StatusMessage = "Denied";
|
||||
if (approval.ItemStatus == 0)
|
||||
approval.StatusMessage = "Assigned";
|
||||
if (approval.ItemStatus > 0)
|
||||
approval.StatusMessage = "Approved";
|
||||
}
|
||||
|
||||
return approvals;
|
||||
}
|
||||
|
||||
public async Task UpdateApproval(Approval approval) {
|
||||
if (approval is null) throw new ArgumentNullException("approval cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Put, "approval");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(approval),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Unable to update approval, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
await GetApprovalsForIssueId(approval.IssueID, true);
|
||||
await GetApprovalsForUserId(approval.UserID, true);
|
||||
}
|
||||
|
||||
public async Task Approve(Approval approval) {
|
||||
if (approval is null) throw new ArgumentNullException("approval cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Put, "approval/approve");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(approval),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Approval failed, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
await GetApprovalsForIssueId(approval.IssueID, true);
|
||||
await GetApprovalsForUserId(approval.UserID, true);
|
||||
}
|
||||
|
||||
public async Task Deny(Approval approval) {
|
||||
if (approval is null) throw new ArgumentNullException("approval cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Put, "approval/deny");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(approval),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Denial failed, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
await GetApprovalsForIssueId(approval.IssueID, true);
|
||||
await GetApprovalsForUserId(approval.UserID, true);
|
||||
}
|
||||
|
||||
public async Task<int> GetRoleIdForRoleName(string roleName) {
|
||||
if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("role name cannot be null or empty");
|
||||
|
||||
int roleId = _cache.Get<int>($"roleId{roleName}");
|
||||
|
||||
if (roleId <= 0) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"approval/roleId?roleName={roleName}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
roleId = JsonSerializer.Deserialize<int>(responseContent, jsonSerializerOptions);
|
||||
|
||||
if (roleId <= 0)
|
||||
throw new Exception($"unable to find role ID for {roleName}");
|
||||
|
||||
_cache.Set($"roleId{roleName}", roleId, DateTimeOffset.Now.AddMinutes(15));
|
||||
} else {
|
||||
throw new Exception($"Unable to get role ID, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SubRole>> GetSubRolesForSubRoleName(string subRoleName, int roleId) {
|
||||
if (string.IsNullOrWhiteSpace(subRoleName)) throw new ArgumentException("role name cannot be null or empty");
|
||||
if (roleId <= 0) throw new ArgumentException($"{roleId} is not a valid role ID");
|
||||
|
||||
IEnumerable<SubRole>? subRoles = _cache.Get<IEnumerable<SubRole>>($"subRoles{subRoleName}");
|
||||
|
||||
if (subRoles is null || subRoles.Count() <= 0) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"approval/subRoles?subRoleName={subRoleName}&roleId={roleId}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
subRoles = JsonSerializer.Deserialize<IEnumerable<SubRole>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse sub roles from API response");
|
||||
|
||||
if (subRoles is not null && subRoles.Count() > 0) {
|
||||
_cache.Set($"subRoles{subRoleName}", subRoles, DateTimeOffset.Now.AddMinutes(15));
|
||||
} else {
|
||||
throw new Exception($"unable to find sub roles for {subRoleName}");
|
||||
}
|
||||
} else {
|
||||
throw new Exception($"Unable to get sub roles, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return subRoles;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<User>> GetApprovalGroupMembers(int subRoleId) {
|
||||
if (subRoleId <= 0) throw new ArgumentException($"{subRoleId} is not a valid sub role ID");
|
||||
|
||||
IEnumerable<User>? members = _cache.Get<IEnumerable<User>>($"approvalMembers{subRoleId}");
|
||||
|
||||
if (members is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"approval/members?subRoleId={subRoleId}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
members = JsonSerializer.Deserialize<IEnumerable<User>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse users from API response");
|
||||
|
||||
if (members is null || members.Count() <= 0)
|
||||
throw new Exception($"unable to find group members for sub role {subRoleId}");
|
||||
|
||||
_cache.Set($"approvalMembers{subRoleId}", members, DateTimeOffset.Now.AddMinutes(15));
|
||||
} else {
|
||||
throw new Exception($"Unable to get group members, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
}
|
187
MesaFabApproval.Client/Services/AuthenticationService.cs
Normal file
187
MesaFabApproval.Client/Services/AuthenticationService.cs
Normal file
@ -0,0 +1,187 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Blazored.SessionStorage;
|
||||
|
||||
using MesaFabApproval.Shared.Models;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace MesaFabApproval.Client.Services;
|
||||
|
||||
public interface IAuthenticationService {
|
||||
Task<ClaimsPrincipal> SendAuthenticationRequest(string loginId, string password);
|
||||
Task<ClaimsPrincipal> FetchAuthState();
|
||||
Task ClearTokens();
|
||||
Task ClearCurrentUser();
|
||||
Task SetTokens(string jwt, string refreshToken);
|
||||
Task SetLoginId(string loginId);
|
||||
Task SetCurrentUser(User user);
|
||||
Task<User> GetCurrentUser();
|
||||
Task<AuthTokens> GetAuthTokens();
|
||||
Task<string> GetLoginId();
|
||||
ClaimsPrincipal GetClaimsPrincipalFromJwt(string jwt);
|
||||
}
|
||||
|
||||
public class AuthenticationService : IAuthenticationService {
|
||||
private readonly ISessionStorageService _sessionStorageService;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public AuthenticationService(ISessionStorageService sessionStorageService,
|
||||
IMemoryCache cache,
|
||||
IHttpClientFactory httpClientFactory) {
|
||||
_sessionStorageService = sessionStorageService ??
|
||||
throw new ArgumentNullException("ISessionStorageService not injected");
|
||||
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException("IHttpClientFactory not injected");
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> SendAuthenticationRequest(string loginId, string password) {
|
||||
if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentException("loginId cannot be null or empty");
|
||||
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("password cannot be null or empty");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
AuthAttempt authAttempt = new() {
|
||||
LoginID = loginId,
|
||||
Password = password
|
||||
};
|
||||
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "auth/login");
|
||||
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(authAttempt),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);
|
||||
|
||||
if (httpResponseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
LoginResult loginResult = JsonSerializer.Deserialize<LoginResult>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse login result from API response");
|
||||
|
||||
if (!loginResult.IsAuthenticated) throw new Exception($"User with Login ID {loginId} not authorized");
|
||||
|
||||
await SetLoginId(loginId);
|
||||
|
||||
await SetTokens(loginResult.AuthTokens.JwtToken, loginResult.AuthTokens.RefreshToken);
|
||||
|
||||
await SetCurrentUser(loginResult.User);
|
||||
|
||||
ClaimsPrincipal principal = GetClaimsPrincipalFromJwt(loginResult.AuthTokens.JwtToken);
|
||||
|
||||
return principal;
|
||||
} else {
|
||||
throw new Exception($"Login API request failed for {loginId}, because {httpResponseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> FetchAuthState() {
|
||||
string? jwt = _cache.Get<string>("MesaFabApprovalJwt");
|
||||
|
||||
if (jwt is null) jwt = await _sessionStorageService.GetItemAsync<string>("MesaFabApprovalJwt");
|
||||
|
||||
if (jwt is null) throw new Exception("Unable to find JWT");
|
||||
|
||||
ClaimsPrincipal principal = GetClaimsPrincipalFromJwt(jwt);
|
||||
|
||||
return principal;
|
||||
}
|
||||
|
||||
public async Task ClearTokens() {
|
||||
_cache.Remove("MesaFabApprovalJwt");
|
||||
await _sessionStorageService.RemoveItemAsync("MesaFabApprovalJwt");
|
||||
_cache.Remove("MesaFabApprovalRefreshToken");
|
||||
await _sessionStorageService.RemoveItemAsync("MesaFabApprovalRefreshToken");
|
||||
}
|
||||
|
||||
public async Task SetTokens(string jwt, string refreshToken) {
|
||||
if (string.IsNullOrWhiteSpace(jwt)) throw new ArgumentNullException("JWT cannot be null or empty");
|
||||
if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException("Refresh token cannot be null or empty");
|
||||
|
||||
_cache.Set<string>("MesaFabApprovalJwt", jwt);
|
||||
await _sessionStorageService.SetItemAsync<string>("MesaFabApprovalJwt", jwt);
|
||||
_cache.Set<string>("MesaFabApprovalRefreshToken", refreshToken);
|
||||
await _sessionStorageService.SetItemAsync<string>("MesaFabApprovalRefreshToken", refreshToken);
|
||||
}
|
||||
|
||||
public async Task SetLoginId(string loginId) {
|
||||
if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentNullException("LoginId cannot be null or empty");
|
||||
|
||||
_cache.Set<string>("MesaFabApprovalUserId", loginId);
|
||||
await _sessionStorageService.SetItemAsync<string>("MesaFabApprovalUserId", loginId);
|
||||
}
|
||||
|
||||
public async Task SetCurrentUser(User user) {
|
||||
if (user is null) throw new ArgumentNullException("User cannot be null");
|
||||
|
||||
_cache.Set<User>("MesaFabApprovalCurrentUser", user);
|
||||
await _sessionStorageService.SetItemAsync<User>("MesaFabApprovalCurrentUser", user);
|
||||
}
|
||||
|
||||
public async Task ClearCurrentUser() {
|
||||
_cache.Remove("MesaFabApprovalCurrentUser");
|
||||
await _sessionStorageService.RemoveItemAsync("MesaFabApprovalCurrentUser");
|
||||
_cache.Remove("MesaFabApprovalUserId");
|
||||
await _sessionStorageService.RemoveItemAsync("MesaFabApprovalUserId");
|
||||
}
|
||||
|
||||
public async Task<User> GetCurrentUser() {
|
||||
User? currentUser = null;
|
||||
|
||||
currentUser = _cache.Get<User>("MesaFabApprovalCurrentUser") ??
|
||||
await _sessionStorageService.GetItemAsync<User>("MesaFabApprovalCurrentUser");
|
||||
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
public async Task<AuthTokens> GetAuthTokens() {
|
||||
AuthTokens? authTokens = null;
|
||||
|
||||
string? jwt = _cache.Get<string>("MesaFabApprovalJwt");
|
||||
if (jwt is null) jwt = await _sessionStorageService.GetItemAsync<string>("MesaFabApprovalJwt");
|
||||
|
||||
string? refreshToken = _cache.Get<string>("MesaFabApprovalRefreshToken");
|
||||
if (refreshToken is null) refreshToken = await _sessionStorageService.GetItemAsync<string>("MesaFabApprovalRefreshToken");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(jwt) && !string.IsNullOrWhiteSpace(refreshToken)) {
|
||||
authTokens = new() {
|
||||
JwtToken = jwt,
|
||||
RefreshToken = refreshToken
|
||||
};
|
||||
}
|
||||
|
||||
return authTokens;
|
||||
}
|
||||
|
||||
public async Task<string> GetLoginId() {
|
||||
string? loginId = _cache.Get<string>("MesaFabApprovalUserId");
|
||||
if (loginId is null) loginId = await _sessionStorageService.GetItemAsync<string>("MesaFabApprovalUserId");
|
||||
|
||||
return loginId;
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetClaimsPrincipalFromJwt(string jwt) {
|
||||
if (string.IsNullOrWhiteSpace(jwt)) throw new ArgumentException("JWT cannot be null or empty");
|
||||
|
||||
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
|
||||
|
||||
if (!tokenHandler.CanReadToken(jwt)) {
|
||||
throw new Exception("Unable to parse JWT from API");
|
||||
}
|
||||
|
||||
JwtSecurityToken jwtSecurityToken = tokenHandler.ReadJwtToken(jwt);
|
||||
|
||||
ClaimsIdentity identity = new ClaimsIdentity(jwtSecurityToken.Claims, "MesaFabApprovalWasm");
|
||||
|
||||
return new(identity);
|
||||
}
|
||||
}
|
537
MesaFabApproval.Client/Services/MRBService.cs
Normal file
537
MesaFabApproval.Client/Services/MRBService.cs
Normal file
@ -0,0 +1,537 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using MesaFabApproval.Shared.Models;
|
||||
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
using MudBlazor;
|
||||
|
||||
namespace MesaFabApproval.Client.Services;
|
||||
|
||||
public interface IMRBService {
|
||||
Task<IEnumerable<MRB>> GetAllMRBs();
|
||||
Task<MRB> GetMRBById(int id);
|
||||
Task<MRB> GetMRBByTitle(string title, bool bypassCache);
|
||||
Task CreateNewMRB(MRB mrb);
|
||||
Task UpdateMRB(MRB mrb);
|
||||
Task SubmitForApproval(MRB mrb);
|
||||
Task GenerateActionTasks(MRB mrb, MRBAction action);
|
||||
Task CreateMRBAction(MRBAction mrbAction);
|
||||
Task<IEnumerable<MRBAction>> GetMRBActionsForMRB(int mrbNumber, bool bypassCache);
|
||||
Task UpdateMRBAction(MRBAction mrbAction);
|
||||
Task DeleteMRBAction(MRBAction mrbAction);
|
||||
Task UploadAttachments(IEnumerable<IBrowserFile> files, int mrbNumber);
|
||||
Task<IEnumerable<MRBAttachment>> GetAllAttachmentsForMRB(int mrbNumber, bool bypassCache);
|
||||
Task DeleteAttachment(MRBAttachment attachment);
|
||||
Task NotifyNewApprovals(MRB mrb);
|
||||
Task NotifyApprovers(MRBNotification notification);
|
||||
Task NotifyOriginator(MRBNotification notification);
|
||||
}
|
||||
|
||||
public class MRBService : IMRBService {
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ISnackbar _snackbar;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IApprovalService _approvalService;
|
||||
|
||||
public MRBService(IMemoryCache cache,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ISnackbar snackbar,
|
||||
IUserService userService,
|
||||
IApprovalService approvalService) {
|
||||
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException("IHttpClientFactory not injected");
|
||||
_snackbar = snackbar ?? throw new ArgumentNullException("ISnackbar not injected");
|
||||
_userService = userService ?? throw new ArgumentNullException("IUserService not injected");
|
||||
_approvalService = approvalService ?? throw new ArgumentNullException("IApprovalService not injected");
|
||||
}
|
||||
|
||||
public async Task CreateNewMRB(MRB mrb) {
|
||||
if (mrb is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, "mrb/new");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(mrb),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Unable to generate new MRB, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
mrb = await GetMRBByTitle(mrb.Title, true);
|
||||
|
||||
_cache.Set($"mrb{mrb.MRBNumber}", mrb, DateTimeOffset.Now.AddHours(1));
|
||||
_cache.Set($"mrb{mrb.Title}", mrb, DateTimeOffset.Now.AddHours(1));
|
||||
|
||||
IEnumerable<MRB>? allMrbs = _cache.Get<IEnumerable<MRB>>("allMrbs");
|
||||
if (allMrbs is not null) {
|
||||
List<MRB> mrbList = allMrbs.ToList();
|
||||
mrbList.Add(mrb);
|
||||
_cache.Set("allMrbs", mrbList);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MRB>> GetAllMRBs() {
|
||||
try {
|
||||
IEnumerable<MRB>? allMRBs = _cache.Get<IEnumerable<MRB>>("allMrbs");
|
||||
|
||||
if (allMRBs is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, "mrb/all");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
allMRBs = JsonSerializer.Deserialize<IEnumerable<MRB>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse MRBs from API response");
|
||||
|
||||
_cache.Set($"allMrbs", allMRBs, DateTimeOffset.Now.AddMinutes(15));
|
||||
} else {
|
||||
throw new Exception($"Unable to get all MRBs, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return allMRBs;
|
||||
} catch (Exception) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
public async Task<MRB> GetMRBById(int id) {
|
||||
if (id <= 0) throw new ArgumentException($"Invalid MRB number: {id}");
|
||||
|
||||
MRB? mrb = _cache.Get<MRB>($"mrb{id}");
|
||||
|
||||
if (mrb is null) mrb = _cache.Get<IEnumerable<MRB>>("allMrbs")?.FirstOrDefault(m => m.MRBNumber == id);
|
||||
|
||||
if (mrb is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"mrb/getById?id={id}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
mrb = JsonSerializer.Deserialize<MRB>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse MRB from API response");
|
||||
|
||||
_cache.Set($"mrb{mrb.MRBNumber}", mrb, DateTimeOffset.Now.AddHours(1));
|
||||
} else {
|
||||
throw new Exception($"Unable to get MRB by Id, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return mrb;
|
||||
}
|
||||
|
||||
public async Task<MRB> GetMRBByTitle(string title, bool bypassCache) {
|
||||
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Title cannot be null or emtpy");
|
||||
|
||||
MRB? mrb = null;
|
||||
if (!bypassCache) mrb = _cache.Get<MRB>($"mrb{title}");
|
||||
|
||||
if (mrb is null) mrb = _cache.Get<IEnumerable<MRB>>("allMrbs")?.FirstOrDefault(m => m.Title.Equals(title));
|
||||
|
||||
if (mrb is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"mrb/getByTitle?title={title}&bypassCache={bypassCache}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
mrb = JsonSerializer.Deserialize<MRB>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse MRB from API response");
|
||||
|
||||
_cache.Set($"mrb{mrb.Title}", mrb, DateTimeOffset.Now.AddHours(1));
|
||||
} else {
|
||||
throw new Exception($"Unable to get MRB by title, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return mrb;
|
||||
}
|
||||
|
||||
public async Task UpdateMRB(MRB mrb) {
|
||||
if (mrb is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Put, $"mrb");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(mrb),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Unable to update MRB, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
_cache.Set($"mrb{mrb.MRBNumber}", mrb, DateTimeOffset.Now.AddHours(1));
|
||||
_cache.Set($"mrb{mrb.Title}", mrb, DateTimeOffset.Now.AddHours(1));
|
||||
|
||||
IEnumerable<MRB>? allMrbs = _cache.Get<IEnumerable<MRB>>("allMrbs");
|
||||
if (allMrbs is not null) {
|
||||
List<MRB> mrbList = allMrbs.ToList();
|
||||
mrbList.RemoveAll(m => m.MRBNumber == mrb.MRBNumber);
|
||||
mrbList.Add(mrb);
|
||||
_cache.Set("allMrbs", mrbList);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateMRBAction(MRBAction mrbAction) {
|
||||
if (mrbAction is null) throw new ArgumentNullException("MRB action cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, "mrbAction");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(mrbAction),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to create new MRB action, because {responseMessage.ReasonPhrase}");
|
||||
|
||||
await GetMRBActionsForMRB(mrbAction.MRBNumber, true);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MRBAction>> GetMRBActionsForMRB(int mrbNumber, bool bypassCache) {
|
||||
if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB#");
|
||||
|
||||
IEnumerable<MRBAction>? mrbActions = null;
|
||||
if (!bypassCache)
|
||||
mrbActions = _cache.Get<IEnumerable<MRBAction>>($"mrbActions{mrbNumber}");
|
||||
|
||||
if (mrbActions is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"mrbAction?mrbNumber={mrbNumber}&bypassCache={bypassCache}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
mrbActions = JsonSerializer.Deserialize<IEnumerable<MRBAction>>(responseContent, jsonSerializerOptions) ??
|
||||
new List<MRBAction>();
|
||||
|
||||
if (mrbActions.Count() > 0)
|
||||
_cache.Set($"mrbActions{mrbNumber}", mrbActions, DateTimeOffset.Now.AddMinutes(5));
|
||||
} else {
|
||||
throw new Exception($"Unable to get MRB {mrbNumber} actions, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return mrbActions;
|
||||
}
|
||||
|
||||
public async Task UpdateMRBAction(MRBAction mrbAction) {
|
||||
if (mrbAction is null) throw new ArgumentNullException("MRB action cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Put, $"mrbAction");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(mrbAction),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
throw new Exception($"Unable to update MRB action, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
IEnumerable<MRBAction>? mrbActions = _cache.Get<IEnumerable<MRBAction>>($"mrbActions{mrbAction.MRBNumber}");
|
||||
if (mrbActions is not null) {
|
||||
List<MRBAction> mrbActionList = mrbActions.ToList();
|
||||
mrbActionList.RemoveAll(a => a.ActionID == mrbAction.ActionID);
|
||||
mrbActionList.Add(mrbAction);
|
||||
_cache.Set($"mrbActions{mrbAction.MRBNumber}", mrbActionList, DateTimeOffset.Now.AddMinutes(5));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteMRBAction(MRBAction mrbAction) {
|
||||
if (mrbAction is null) throw new ArgumentNullException("MRB action cannot be null");
|
||||
if (mrbAction.ActionID <= 0) throw new ArgumentException($"{mrbAction.ActionID} is not a valid MRBActionID");
|
||||
if (mrbAction.MRBNumber <= 0) throw new ArgumentException($"{mrbAction.MRBNumber} is not a valid MRBNumber");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
string route = $"mrbAction?mrbActionID={mrbAction.ActionID}&mrbNumber={mrbAction.MRBNumber}";
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Delete, route);
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to delete MRB action {mrbAction.ActionID}");
|
||||
|
||||
IEnumerable<MRBAction>? mrbActions = _cache.Get<IEnumerable<MRBAction>>($"mrbActions{mrbAction.MRBNumber}");
|
||||
if (mrbActions is not null) {
|
||||
List<MRBAction> mrbActionList = mrbActions.ToList();
|
||||
mrbActionList.RemoveAll(a => a.ActionID == mrbAction.ActionID);
|
||||
_cache.Set($"mrbActions{mrbAction.MRBNumber}", mrbActionList, DateTimeOffset.Now.AddMinutes(5));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadAttachments(IEnumerable<IBrowserFile> files, int mrbNumber) {
|
||||
if (files is null) throw new ArgumentNullException("Files cannot be null");
|
||||
if (files.Count() <= 0) throw new ArgumentException("Files cannot be empty");
|
||||
if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB number");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, $"mrb/attach?mrbNumber={mrbNumber}");
|
||||
|
||||
using MultipartFormDataContent content = new MultipartFormDataContent();
|
||||
|
||||
foreach (IBrowserFile file in files) {
|
||||
try {
|
||||
StreamContent fileContent = new StreamContent(file.OpenReadStream());
|
||||
|
||||
FileExtensionContentTypeProvider contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
|
||||
const string defaultContentType = "application/octet-stream";
|
||||
|
||||
if (!contentTypeProvider.TryGetContentType(file.Name, out string? contentType)) {
|
||||
contentType = defaultContentType;
|
||||
}
|
||||
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||
|
||||
content.Add(content: fileContent, name: "\"files\"", fileName: file.Name);
|
||||
} catch (Exception ex) {
|
||||
_snackbar.Add($"File {file.Name} not saved, because {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
requestMessage.Content = content;
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to save attachments, because {responseMessage.ReasonPhrase}");
|
||||
|
||||
await GetAllAttachmentsForMRB(mrbNumber, true);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MRBAttachment>> GetAllAttachmentsForMRB(int mrbNumber, bool bypassCache) {
|
||||
if (mrbNumber <= 0) throw new ArgumentException($"{mrbNumber} is not a valid MRB#");
|
||||
|
||||
IEnumerable<MRBAttachment>? mrbAttachments = null;
|
||||
if (!bypassCache)
|
||||
mrbAttachments = _cache.Get<IEnumerable<MRBAttachment>>($"mrbAttachments{mrbNumber}");
|
||||
|
||||
if (mrbAttachments is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"mrb/attachments?mrbNumber={mrbNumber}&bypassCache={bypassCache}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
mrbAttachments = JsonSerializer.Deserialize<IEnumerable<MRBAttachment>>(responseContent, jsonSerializerOptions) ??
|
||||
new List<MRBAttachment>();
|
||||
|
||||
if (mrbAttachments.Count() > 0)
|
||||
_cache.Set($"mrbAttachments{mrbNumber}", mrbAttachments, DateTimeOffset.Now.AddMinutes(5));
|
||||
} else {
|
||||
throw new Exception($"Unable to get MRB {mrbNumber} attachments, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
return mrbAttachments;
|
||||
}
|
||||
|
||||
public async Task DeleteAttachment(MRBAttachment attachment) {
|
||||
if (attachment is null) throw new ArgumentNullException("MRB attachment cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Delete, "mrb/attach");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(attachment),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to delete MRB attachment");
|
||||
|
||||
IEnumerable<MRBAttachment>? mrbAttachments = _cache.Get<IEnumerable<MRBAttachment>>($"mrbAttachments{attachment.MRBNumber}");
|
||||
if (mrbAttachments is not null) {
|
||||
List<MRBAttachment> mrbAttachmentList = mrbAttachments.ToList();
|
||||
mrbAttachmentList.RemoveAll(a => a.AttachmentID == attachment.AttachmentID);
|
||||
_cache.Set($"mrbAttachments{attachment.MRBNumber}", mrbAttachmentList, DateTimeOffset.Now.AddMinutes(5));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubmitForApproval(MRB mrb) {
|
||||
if (mrb is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
|
||||
string roleName = "QA_PRE_APPROVAL";
|
||||
string subRoleName = "QA_PRE_APPROVAL";
|
||||
|
||||
if (mrb.StageNo > 1) {
|
||||
roleName = "MRB Approver";
|
||||
subRoleName = "MRBApprover";
|
||||
}
|
||||
|
||||
int roleId = await _approvalService.GetRoleIdForRoleName(roleName);
|
||||
|
||||
if (roleId <= 0) throw new Exception($"could not find {roleName} role ID");
|
||||
|
||||
IEnumerable<SubRole> subRoles = await _approvalService.GetSubRolesForSubRoleName(subRoleName, roleId);
|
||||
|
||||
foreach (SubRole subRole in subRoles) {
|
||||
IEnumerable<User> members = await _approvalService.GetApprovalGroupMembers(subRole.SubRoleID);
|
||||
|
||||
foreach (User member in members) {
|
||||
Approval approval = new() {
|
||||
IssueID = mrb.MRBNumber,
|
||||
RoleName = roleName,
|
||||
SubRole = subRole.SubRoleName,
|
||||
UserID = member.UserID,
|
||||
SubRoleID = subRole.SubRoleID,
|
||||
AssignedDate = DateTime.Now,
|
||||
Step = mrb.StageNo
|
||||
};
|
||||
|
||||
await _approvalService.CreateApproval(approval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GenerateActionTasks(MRB mrb, MRBAction action) {
|
||||
if (mrb is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
if (action is null) throw new ArgumentNullException("MRBAction cannot be null");
|
||||
|
||||
string roleName = "MRB Actions";
|
||||
string subRoleName = "MRBActions";
|
||||
|
||||
int roleId = await _approvalService.GetRoleIdForRoleName(roleName);
|
||||
|
||||
if (roleId <= 0) throw new Exception($"could not find {roleName} role ID");
|
||||
|
||||
IEnumerable<SubRole> subRoles = await _approvalService.GetSubRolesForSubRoleName(subRoleName, roleId);
|
||||
|
||||
foreach (SubRole subRole in subRoles) {
|
||||
IEnumerable<User> members = await _approvalService.GetApprovalGroupMembers(subRole.SubRoleID);
|
||||
|
||||
foreach (User member in members) {
|
||||
Approval approval = new() {
|
||||
IssueID = action.MRBNumber,
|
||||
RoleName = roleName,
|
||||
SubRole = subRole.SubRoleName,
|
||||
UserID = member.UserID,
|
||||
SubRoleID = subRole.SubRoleID,
|
||||
AssignedDate = DateTime.Now,
|
||||
Step = mrb.StageNo,
|
||||
SubRoleCategoryItem = subRole.SubRoleCategoryItem,
|
||||
TaskID = action.ActionID
|
||||
};
|
||||
|
||||
await _approvalService.CreateApproval(approval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task NotifyNewApprovals(MRB mrb) {
|
||||
if (mrb is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, $"mrb/notify/new-approvals");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(mrb),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to notify new MRB approvers, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
public async Task NotifyApprovers(MRBNotification notification) {
|
||||
if (notification is null) throw new ArgumentNullException("notification cannot be null");
|
||||
if (notification.MRB is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, $"mrb/notify/approvers");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(notification),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to notify MRB approvers, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
|
||||
public async Task NotifyOriginator(MRBNotification notification) {
|
||||
if (notification is null) throw new ArgumentNullException("notification cannot be null");
|
||||
if (notification.MRB is null) throw new ArgumentNullException("MRB cannot be null");
|
||||
if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty");
|
||||
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, $"mrb/notify/originator");
|
||||
|
||||
requestMessage.Content = new StringContent(JsonSerializer.Serialize(notification),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
throw new Exception($"Unable to notify MRB originator, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
using MesaFabApproval.Shared.Models;
|
||||
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
using MudBlazor;
|
||||
|
||||
namespace MesaFabApproval.Client.Services;
|
||||
|
||||
public class MesaFabApprovalAuthStateProvider : AuthenticationStateProvider, IDisposable {
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISnackbar _snackbar;
|
||||
|
||||
public User? CurrentUser { get; private set; }
|
||||
|
||||
public MesaFabApprovalAuthStateProvider(IAuthenticationService authService,
|
||||
ISnackbar snackbar,
|
||||
IUserService userService) {
|
||||
_authService = authService ??
|
||||
throw new ArgumentNullException("IAuthenticationService not injected");
|
||||
_snackbar = snackbar ??
|
||||
throw new ArgumentNullException("ISnackbar not injected");
|
||||
_userService = userService ??
|
||||
throw new ArgumentNullException("IUserService not injected");
|
||||
AuthenticationStateChanged += OnAuthenticationStateChangedAsync;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
|
||||
ClaimsPrincipal principal = new();
|
||||
try {
|
||||
principal = await _authService.FetchAuthState();
|
||||
|
||||
CurrentUser = await _authService.GetCurrentUser();
|
||||
|
||||
return new(principal);
|
||||
} catch (Exception ex) {
|
||||
return new(new ClaimsPrincipal());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StateHasChanged(ClaimsPrincipal principal) {
|
||||
if (principal is null) throw new ArgumentNullException("ClaimsPrincipal cannot be null");
|
||||
|
||||
CurrentUser = await _authService.GetCurrentUser();
|
||||
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal)));
|
||||
}
|
||||
|
||||
public async Task LoginAsync(string loginId, string password) {
|
||||
try {
|
||||
if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentException("LoginId cannot be null or empty");
|
||||
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Password cannot be null or empty");
|
||||
|
||||
ClaimsPrincipal principal = await _authService.SendAuthenticationRequest(loginId, password);
|
||||
|
||||
CurrentUser = await _authService.GetCurrentUser();
|
||||
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal)));
|
||||
} catch (Exception ex) {
|
||||
_snackbar.Add(ex.Message, Severity.Error);
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new())));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Logout() {
|
||||
CurrentUser = null;
|
||||
await _authService.ClearTokens();
|
||||
await _authService.ClearCurrentUser();
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new())));
|
||||
}
|
||||
|
||||
public void Dispose() => AuthenticationStateChanged -= OnAuthenticationStateChangedAsync;
|
||||
|
||||
private async void OnAuthenticationStateChangedAsync(Task<AuthenticationState> task) {
|
||||
try {
|
||||
AuthenticationState authenticationState = await task;
|
||||
if (authenticationState is not null) {
|
||||
ClaimsPrincipal principal = await _authService.FetchAuthState();
|
||||
|
||||
CurrentUser = await _authService.GetCurrentUser();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Unable to get authentication state, because {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
207
MesaFabApproval.Client/Services/UserService.cs
Normal file
207
MesaFabApproval.Client/Services/UserService.cs
Normal file
@ -0,0 +1,207 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
using MesaFabApproval.Shared.Models;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace MesaFabApproval.Client.Services;
|
||||
|
||||
public interface IUserService {
|
||||
ClaimsPrincipal GetClaimsPrincipalFromUser(User user);
|
||||
string GetLoginIdFromClaimsPrincipal(ClaimsPrincipal claimsPrincipal);
|
||||
Task<User> GetUserFromClaimsPrincipal(ClaimsPrincipal claimsPrincipal);
|
||||
Task<User> GetUserByUserId(int userId);
|
||||
Task<User> GetUserByLoginId(string loginId);
|
||||
Task<IEnumerable<User>> GetAllActiveUsers();
|
||||
Task<IEnumerable<int>> GetApproverUserIdsBySubRoleCategoryItem(string item);
|
||||
}
|
||||
|
||||
public class UserService : IUserService {
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public UserService(IMemoryCache cache, IHttpClientFactory httpClientFactory) {
|
||||
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException("IHttpClientFactory not injected");
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetClaimsPrincipalFromUser(User user) {
|
||||
if (user is null) throw new ArgumentNullException("user cannot be null");
|
||||
|
||||
List<Claim> claims = new() {
|
||||
new Claim(nameof(user.LoginID), user.LoginID)
|
||||
};
|
||||
|
||||
if (user.IsManager) claims.Add(new Claim(ClaimTypes.Role, "manager"));
|
||||
if (user.IsAdmin) claims.Add(new Claim(ClaimTypes.Role, "admin"));
|
||||
|
||||
ClaimsIdentity identity = new ClaimsIdentity(claims, "MesaFabApprovalWasm");
|
||||
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
|
||||
public async Task<User> GetUserByUserId(int userId) {
|
||||
if (userId <= 0) throw new ArgumentException($"{userId} is not a valid user ID");
|
||||
|
||||
User? user = _cache.Get<User>($"user{userId}");
|
||||
|
||||
if (user is null)
|
||||
user = _cache.Get<IEnumerable<User>>("allActiveUsers")?.FirstOrDefault(u => u.UserID == userId);
|
||||
|
||||
if (user is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"user/userId?userId={userId}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
user = JsonSerializer.Deserialize<User>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse user from API response");
|
||||
|
||||
_cache.Set($"user{userId}", user, DateTimeOffset.Now.AddDays(1));
|
||||
} else {
|
||||
throw new Exception($"GetUserByUserId failed for user {userId}, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
if (user is null) throw new Exception($"User for userId {userId} not found");
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<User> GetUserByLoginId(string loginId) {
|
||||
if (string.IsNullOrWhiteSpace(loginId))
|
||||
throw new ArgumentNullException("loginId cannot be null or empty");
|
||||
|
||||
User? user = _cache.Get<User>($"user{loginId}");
|
||||
|
||||
if (user is null)
|
||||
user = _cache.Get<IEnumerable<User>>("allActiveUsers")?.FirstOrDefault(u => u.LoginID == loginId);
|
||||
|
||||
if (user is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"user/loginId?loginId={loginId}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
user = JsonSerializer.Deserialize<User>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse user from API response");
|
||||
|
||||
_cache.Set($"user{loginId}", user, DateTimeOffset.Now.AddDays(1));
|
||||
} else {
|
||||
throw new Exception($"GetUserByLoginId failed for {loginId}, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
if (user is null) throw new Exception($"User for loginId {loginId} not found");
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<User>> GetAllActiveUsers() {
|
||||
IEnumerable<User>? activeUsers = _cache.Get<IEnumerable<User>>("allActiveUsers");
|
||||
|
||||
if (activeUsers is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"users/active");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
activeUsers = JsonSerializer.Deserialize<IEnumerable<User>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse user from API response");
|
||||
|
||||
_cache.Set("allActiveUsers", activeUsers, DateTimeOffset.Now.AddHours(1));
|
||||
} else {
|
||||
throw new Exception($"Cannot get all active users, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
if (activeUsers is null)
|
||||
activeUsers = new List<User>();
|
||||
|
||||
return activeUsers;
|
||||
}
|
||||
|
||||
public string GetLoginIdFromClaimsPrincipal(ClaimsPrincipal principal) {
|
||||
if (principal is null) throw new ArgumentNullException("Principal cannot be null");
|
||||
|
||||
Claim loginIdClaim = principal.FindFirst("LoginID") ??
|
||||
throw new Exception("LoginID claim not found in principal");
|
||||
|
||||
string loginId = loginIdClaim.Value;
|
||||
|
||||
return loginId;
|
||||
}
|
||||
|
||||
public async Task<User> GetUserFromClaimsPrincipal(ClaimsPrincipal claimsPrincipal) {
|
||||
if (claimsPrincipal is null) throw new ArgumentNullException("ClaimsPrincipal cannot be null");
|
||||
|
||||
Claim loginIdClaim = claimsPrincipal.FindFirst("LoginID") ??
|
||||
throw new Exception("LoginID claim not found in principal");
|
||||
|
||||
string loginId = loginIdClaim.Value ??
|
||||
throw new Exception("LoginID claim value is null");
|
||||
|
||||
User user = await GetUserByLoginId(loginId) ??
|
||||
throw new Exception($"User for loginId {loginId} not found");
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>> GetApproverUserIdsBySubRoleCategoryItem(string item) {
|
||||
if (string.IsNullOrWhiteSpace(item)) throw new ArgumentException("SubRoleCategoryItem cannot be null or empty");
|
||||
|
||||
IEnumerable<int>? approverUserIds = _cache.Get<IEnumerable<int>>($"approvers{item}");
|
||||
|
||||
if (approverUserIds is null) {
|
||||
HttpClient httpClient = _httpClientFactory.CreateClient("API");
|
||||
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, $"approver?subRoleCategoryItem={item}");
|
||||
|
||||
HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage);
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await responseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
approverUserIds = JsonSerializer.Deserialize<IEnumerable<int>>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse user from API response");
|
||||
|
||||
_cache.Set($"approvers{item}", approverUserIds, DateTimeOffset.Now.AddDays(1));
|
||||
} else {
|
||||
throw new Exception($"Unable to get approvers for SubRoleCategoryItem {item}, because {responseMessage.ReasonPhrase}");
|
||||
}
|
||||
}
|
||||
|
||||
if (approverUserIds is null) throw new Exception($"Approvers for SubRoleCategoryItem {item} not found");
|
||||
|
||||
return approverUserIds;
|
||||
}
|
||||
}
|
11
MesaFabApproval.Client/Util/DateTimeUtilities.cs
Normal file
11
MesaFabApproval.Client/Util/DateTimeUtilities.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace MesaFabApproval.Client.Util;
|
||||
|
||||
public class DateTimeUtilities {
|
||||
public static string GetDateAsStringMinDefault(DateTime dt) {
|
||||
return dt > DateTime.MinValue ? dt.ToString("yyyy-MM-dd HH:mm") : "";
|
||||
}
|
||||
|
||||
public static string GetDateAsStringMaxDefault(DateTime dt) {
|
||||
return dt < DateTime.MaxValue ? dt.ToString("yyyy-MM-dd HH:mm") : "";
|
||||
}
|
||||
}
|
107
MesaFabApproval.Client/Utilities/ApiHttpClientHandler.cs
Normal file
107
MesaFabApproval.Client/Utilities/ApiHttpClientHandler.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
|
||||
using MesaFabApproval.Shared.Models;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Net.Http.Headers;
|
||||
using MesaFabApproval.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace MesaFabApproval.Client.Utilities;
|
||||
|
||||
public class ApiHttpClientHandler : DelegatingHandler {
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly MesaFabApprovalAuthStateProvider _authStateProvider;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
|
||||
private readonly string _apiBaseUrl;
|
||||
|
||||
public ApiHttpClientHandler(IMemoryCache cache,
|
||||
IConfiguration config,
|
||||
IAuthenticationService authService,
|
||||
MesaFabApprovalAuthStateProvider authStateProvider,
|
||||
NavigationManager navigationManager) {
|
||||
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
|
||||
_apiBaseUrl = config["FabApprovalApiBaseUrl"] ??
|
||||
throw new NullReferenceException("FabApprovalApiBaseUrl not found in config");
|
||||
_authService = authService ??
|
||||
throw new ArgumentNullException("IAuthenticationService not injected");
|
||||
_authStateProvider = authStateProvider ??
|
||||
throw new ArgumentNullException("MesaFabApprovalAuthStateProvider not injected");
|
||||
_navigationManager = navigationManager ??
|
||||
throw new ArgumentNullException("NavigationManager not injected");
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage,
|
||||
CancellationToken cancellationToken) {
|
||||
AuthTokens? authTokens = await _authService.GetAuthTokens();
|
||||
|
||||
if (authTokens is not null) requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authTokens.JwtToken);
|
||||
|
||||
HttpResponseMessage responseMessage = await base.SendAsync(requestMessage, cancellationToken);
|
||||
|
||||
if (responseMessage.StatusCode.Equals(HttpStatusCode.Unauthorized)) {
|
||||
string? loginId = await _authService.GetLoginId();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(loginId) && authTokens is not null) {
|
||||
AuthAttempt authAttempt = new() {
|
||||
LoginID = loginId,
|
||||
AuthTokens = authTokens
|
||||
};
|
||||
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "auth/refresh");
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authTokens.JwtToken);
|
||||
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(authAttempt),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
HttpClient httpClient = new HttpClient() {
|
||||
BaseAddress = new Uri(_apiBaseUrl)
|
||||
};
|
||||
|
||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
|
||||
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (httpResponseMessage.IsSuccessStatusCode) {
|
||||
string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
|
||||
|
||||
JsonSerializerOptions jsonSerializerOptions = new() {
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
LoginResult loginResult = JsonSerializer.Deserialize<LoginResult>(responseContent, jsonSerializerOptions) ??
|
||||
throw new Exception("Unable to parse login result from API response");
|
||||
|
||||
if (!loginResult.IsAuthenticated) throw new Exception($"User with Login ID {loginId} not authorized");
|
||||
|
||||
if (loginResult.AuthTokens is not null) {
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", loginResult.AuthTokens.JwtToken);
|
||||
await _authService.SetTokens(loginResult.AuthTokens.JwtToken, loginResult.AuthTokens.RefreshToken);
|
||||
}
|
||||
|
||||
if (loginResult.User is not null)
|
||||
await _authService.SetCurrentUser(loginResult.User);
|
||||
} else {
|
||||
await _authStateProvider.Logout();
|
||||
|
||||
string? redirectUrl = _cache.Get<string>("redirectUrl");
|
||||
if (!string.IsNullOrWhiteSpace(redirectUrl)) {
|
||||
_navigationManager.NavigateTo($"login/{redirectUrl}");
|
||||
} else {
|
||||
_navigationManager.NavigateTo("login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await base.SendAsync(requestMessage, cancellationToken);
|
||||
}
|
||||
|
||||
return responseMessage;
|
||||
}
|
||||
}
|
22
MesaFabApproval.Client/_Imports.razor
Normal file
22
MesaFabApproval.Client/_Imports.razor
Normal file
@ -0,0 +1,22 @@
|
||||
@using System.IdentityModel.Tokens.Jwt
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using MesaFabApproval.Client
|
||||
@using MesaFabApproval.Client.Layout
|
||||
@using MesaFabApproval.Client.Pages.Components
|
||||
@using MesaFabApproval.Client.Services
|
||||
@using MesaFabApproval.Client.Util
|
||||
@using MesaFabApproval.Shared.Models
|
||||
@using MudBlazor
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.Extensions.Caching.Memory
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
@attribute [Authorize]
|
BIN
MesaFabApproval.Client/wwwroot/android-chrome-192x192.png
Normal file
BIN
MesaFabApproval.Client/wwwroot/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
MesaFabApproval.Client/wwwroot/android-chrome-512x512.png
Normal file
BIN
MesaFabApproval.Client/wwwroot/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
MesaFabApproval.Client/wwwroot/apple-touch-icon.png
Normal file
BIN
MesaFabApproval.Client/wwwroot/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
4
MesaFabApproval.Client/wwwroot/appsettings.json
Normal file
4
MesaFabApproval.Client/wwwroot/appsettings.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"OldFabApprovalUrl": "https://mesaapproval-test.mes.infineon.com",
|
||||
"FabApprovalApiBaseUrl": "https://mesaapproval-test.mes.infineon.com:7114"
|
||||
}
|
103
MesaFabApproval.Client/wwwroot/css/app.css
Normal file
103
MesaFabApproval.Client/wwwroot/css/app.css
Normal file
@ -0,0 +1,103 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
7
MesaFabApproval.Client/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
7
MesaFabApproval.Client/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
MesaFabApproval.Client/wwwroot/favicon-16x16.png
Normal file
BIN
MesaFabApproval.Client/wwwroot/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 446 B |
BIN
MesaFabApproval.Client/wwwroot/favicon-32x32.png
Normal file
BIN
MesaFabApproval.Client/wwwroot/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 865 B |
BIN
MesaFabApproval.Client/wwwroot/favicon.ico
Normal file
BIN
MesaFabApproval.Client/wwwroot/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
49
MesaFabApproval.Client/wwwroot/index.html
Normal file
49
MesaFabApproval.Client/wwwroot/index.html
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mesa Fab Approval</title>
|
||||
<base href="/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
<link href="MesaFabApproval.Client.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||
<script>
|
||||
if (window.location.hostname.includes("localhost")) {
|
||||
Blazor.start({
|
||||
webAssembly: {
|
||||
environment: "Development"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Blazor.start();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
1
MesaFabApproval.Client/wwwroot/site.webmanifest
Normal file
1
MesaFabApproval.Client/wwwroot/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"Mesa Fab Approval","short_name":"MesaFabApproval","icons":[{"src":"android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
Reference in New Issue
Block a user