Compare commits
165 Commits
main
...
OT_Project
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7c5757e0 | ||
|
|
efd69601ec | ||
|
|
5efe8e13c4 | ||
|
|
19a9ade3eb | ||
|
|
6d7dc52724 | ||
|
|
4248484877 | ||
|
|
c0c2c59ea3 | ||
|
|
90e56547ba | ||
|
|
8a39714a25 | ||
|
|
be8084ca85 | ||
|
|
695af0f339 | ||
|
|
1400532680 | ||
|
|
872eb2363a | ||
|
|
ffdc93a4b7 | ||
|
|
2c9d8bc4da | ||
|
|
95db83ab3e | ||
|
|
db408bb2db | ||
|
|
32d8689eb1 | ||
|
|
9e3539caa6 | ||
|
|
aa316b94ae | ||
|
|
802a81357e | ||
|
|
e131348fb8 | ||
|
|
c338ee7c6c | ||
|
|
bdb17f766c | ||
|
|
f9bc489b8e | ||
|
|
de191e7943 | ||
|
|
1d02b87c1d | ||
|
|
4a7368b4d7 | ||
|
|
cf4b5fe8b5 | ||
|
|
f73eecdf77 | ||
|
|
54367ee3cd | ||
| ed04371e3c | |||
| 21a950f519 | |||
| cc99419e49 | |||
| 6c2923ca1a | |||
| 53bbde0f0b | |||
| 2d474477fa | |||
| 1e49f979aa | |||
| 02197f4c1f | |||
| 9ed5e6b6a5 | |||
| d316a42a5e | |||
| 9857983174 | |||
| 0225c22696 | |||
| e43205c08e | |||
| a3dc02fb80 | |||
| e78e1c979e | |||
| ea9038d6b8 | |||
| 1542750302 | |||
| 37ea90be65 | |||
| ea54070d54 | |||
| ce941d5d53 | |||
| 48636528b1 | |||
| c36a1e6094 | |||
| dd23c13644 | |||
| 1411c0c830 | |||
| aaa93edb89 | |||
| 51150e5377 | |||
| fdc3c81574 | |||
| 00396e4b0f | |||
| 6018c3df2c | |||
| f4fc5dc103 | |||
| e64e288eac | |||
| f6f2990c8f | |||
| 98161fd740 | |||
| 7e5f3e3b36 | |||
| f8be5be392 | |||
| 3ec456afbd | |||
| a0d84272aa | |||
| 26aaff2d2b | |||
| eab78d321f | |||
| 4afac02583 | |||
| 18ecaedda8 | |||
| eba3f6c5c4 | |||
| b7ec8ab7af | |||
| f4125a09ed | |||
| 11e8e84064 | |||
| 6150baa25d | |||
| 9d9f931642 | |||
| 38a9e623a8 | |||
| 976a83bbcc | |||
| 2d574ad949 | |||
| dde2da8d41 | |||
| 00264dd2d2 | |||
| a7ffd18754 | |||
| e1cf4fc885 | |||
| 0ff7592b01 | |||
| df2ec7e88c | |||
| 98ae0e1ad6 | |||
| ebf8008b22 | |||
| 788ab1aa5a | |||
| 41d1c8e1af | |||
| 02f8ce4cd8 | |||
| 01814cc26f | |||
| 20c01825f9 | |||
| e28117b578 | |||
| 8cf3fd9b14 | |||
| 31254a6460 | |||
| 2cdf176620 | |||
| 79fb9649d7 | |||
| ea4666690e | |||
| 43962646bb | |||
| ca46776473 | |||
| 0f2b065c66 | |||
| 9214a081d3 | |||
| 646136aade | |||
| 1c4e1a32c6 | |||
| 531106d90d | |||
| 92ce4b6267 | |||
| 4d517fdd6c | |||
| 8b7a4d5390 | |||
| a6572b6b22 | |||
| e478637fba | |||
| d8850efed7 | |||
| 1e1b65dce6 | |||
| 85fce16dbe | |||
| d9e67e6139 | |||
| 5f4e8c6c22 | |||
| f8ef2b449c | |||
| 9b31a50115 | |||
| d7ea028d82 | |||
| 9324f61d05 | |||
| d1682750dc | |||
| 38c4629302 | |||
| 6e9ec353b9 | |||
| 0d511921e8 | |||
| 0bf343b65d | |||
| 52421e9693 | |||
| 5576acc67d | |||
| 5a786d221d | |||
| 082be76c51 | |||
| 292f516e33 | |||
| 8ddf9752f4 | |||
| b5d3829457 | |||
| a1e2bf6ae0 | |||
| 3610536233 | |||
| 3cfcfd7a3a | |||
| 45a94c99fc | |||
| 970ece5602 | |||
| 5db84d96a2 | |||
| 7cbc870f78 | |||
| 907a171616 | |||
| 8135725180 | |||
| 391a359a9f | |||
| 348206d306 | |||
| 961b8c6db5 | |||
| cf714b92f6 | |||
| 18d053800a | |||
| fd34ac4822 | |||
| 0ee2817376 | |||
| 0168aa1929 | |||
| caac6c0d39 | |||
| 05a07ca5ea | |||
| 72dbbde075 | |||
| b378c73152 | |||
| 09fa8fc604 | |||
| d7e93a98b3 | |||
| 87c447e935 | |||
| b6b8b4a705 | |||
| 630a3175ce | |||
| 5910e4f15f | |||
| 5569a72e68 | |||
| 54b2affe28 | |||
| 3f3ad980b9 | |||
| 8dc8dd90da | |||
| f51c8ffe65 |
5
.config/dotnet-tools.json
Normal file
5
.config/dotnet-tools.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {}
|
||||
}
|
||||
83
Areas/Identity/Pages/Account/AccessDenied.cshtml
Normal file
83
Areas/Identity/Pages/Account/AccessDenied.cshtml
Normal file
@ -0,0 +1,83 @@
|
||||
@page
|
||||
@model AccessDeniedModel
|
||||
@{
|
||||
ViewData["Title"] = "Access denied";
|
||||
@inject UserManager<UserModel> _userManager
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user != null)
|
||||
{
|
||||
var userComDept = user.departmentId;
|
||||
var userRole = await _userManager.GetRolesAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
<header id="deniedHeader">
|
||||
<template v-if="ldapUserInfo.role.length == 0"><p class="text-danger">You do not have access to this resource because you have no role. Please contact the system administrator.</p></template>
|
||||
<template v-else><p class="text-danger">You do not have access to this resource.</p></template>
|
||||
</header>
|
||||
@section Scripts {
|
||||
<script>
|
||||
if (typeof jQuery === 'undefined') {
|
||||
console.error('jQuery is not loaded.');
|
||||
}
|
||||
$(function () {
|
||||
app.mount('#deniedHeader');
|
||||
});
|
||||
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
ldapUserInfo: {
|
||||
role: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getUserInfo();
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
async getUserInfo() {
|
||||
try {
|
||||
// Show the loading modal
|
||||
$('#loadingModal').modal('show');
|
||||
|
||||
// Perform the fetch request
|
||||
const response = await fetch('/IdentityAPI/GetUserInformation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Check if the response is OK
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.userInfo) {
|
||||
console.log(data.userInfo)
|
||||
this.ldapUserInfo = data.userInfo
|
||||
} else {
|
||||
console.error('Get user failed:', data);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error getting user information:', error);
|
||||
}
|
||||
finally {
|
||||
await new Promise(resolve => {
|
||||
$('#loadingModal').on('shown.bs.modal', resolve);
|
||||
});
|
||||
$('#loadingModal').modal('hide');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}
|
||||
23
Areas/Identity/Pages/Account/AccessDenied.cshtml.cs
Normal file
23
Areas/Identity/Pages/Account/AccessDenied.cshtml.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class AccessDeniedModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Areas/Identity/Pages/Account/ConfirmEmail.cshtml
Normal file
8
Areas/Identity/Pages/Account/ConfirmEmail.cshtml
Normal file
@ -0,0 +1,8 @@
|
||||
@page
|
||||
@model ConfirmEmailModel
|
||||
@{
|
||||
ViewData["Title"] = "Confirm email";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||
52
Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
52
Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ConfirmEmailModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
|
||||
public ConfirmEmailModel(UserManager<UserModel> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
public async Task<IActionResult> OnGetAsync(string userId, string code)
|
||||
{
|
||||
if (userId == null || code == null)
|
||||
{
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml
Normal file
8
Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml
Normal file
@ -0,0 +1,8 @@
|
||||
@page
|
||||
@model ConfirmEmailChangeModel
|
||||
@{
|
||||
ViewData["Title"] = "Confirm email change";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||
70
Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs
Normal file
70
Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs
Normal file
@ -0,0 +1,70 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ConfirmEmailChangeModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
|
||||
public ConfirmEmailChangeModel(UserManager<UserModel> userManager, SignInManager<UserModel> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string userId, string email, string code)
|
||||
{
|
||||
if (userId == null || email == null || code == null)
|
||||
{
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
||||
var result = await _userManager.ChangeEmailAsync(user, email, code);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
StatusMessage = "Error changing email.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
// In our UI email and user name are one and the same, so when we update the email
|
||||
// we need to update the user name.
|
||||
var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
|
||||
if (!setUserNameResult.Succeeded)
|
||||
{
|
||||
StatusMessage = "Error changing user name.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Thank you for confirming your email change.";
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
33
Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
@ -0,0 +1,33 @@
|
||||
@page
|
||||
@model ExternalLoginModel
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h2 id="external-login-title">Associate your @Model.ProviderDisplayName account.</h2>
|
||||
<hr />
|
||||
|
||||
<p id="external-login-description" class="text-info">
|
||||
You've successfully authenticated with <strong>@Model.ProviderDisplayName</strong>.
|
||||
Please enter an email address for this site below and click the Register button to finish
|
||||
logging in.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-page-handler="Confirmation" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email."/>
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
224
Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
224
Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
@ -0,0 +1,224 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ExternalLoginModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly IUserStore<UserModel> _userStore;
|
||||
private readonly IUserEmailStore<UserModel> _emailStore;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILogger<ExternalLoginModel> _logger;
|
||||
|
||||
public ExternalLoginModel(
|
||||
SignInManager<UserModel> signInManager,
|
||||
UserManager<UserModel> userManager,
|
||||
IUserStore<UserModel> userStore,
|
||||
ILogger<ExternalLoginModel> logger,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_userStore = userStore;
|
||||
_emailStore = GetEmailStore();
|
||||
_logger = logger;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ProviderDisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public IActionResult OnGet() => RedirectToPage("./Login");
|
||||
|
||||
public IActionResult OnPost(string provider, string returnUrl = null)
|
||||
{
|
||||
// Request a redirect to the external login provider.
|
||||
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return new ChallengeResult(provider, properties);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
|
||||
{
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
if (remoteError != null)
|
||||
{
|
||||
ErrorMessage = $"Error from external provider: {remoteError}";
|
||||
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
ErrorMessage = "Error loading external login information.";
|
||||
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login.
|
||||
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("{CompanyName} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not have an account, then ask the user to create an account.
|
||||
ReturnUrl = returnUrl;
|
||||
ProviderDisplayName = info.ProviderDisplayName;
|
||||
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
Email = info.Principal.FindFirstValue(ClaimTypes.Email)
|
||||
};
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
|
||||
{
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
// Get the information about the user from the external login provider
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
ErrorMessage = "Error loading external login information during confirmation.";
|
||||
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = CreateUser();
|
||||
|
||||
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
|
||||
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
|
||||
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await _userManager.AddLoginAsync(user, info);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created an account using {CompanyName} provider.", info.LoginProvider);
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = code },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
// If account confirmation is required, we need to show the link if we don't have a real email sender
|
||||
if (_userManager.Options.SignIn.RequireConfirmedAccount)
|
||||
{
|
||||
return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
}
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
ProviderDisplayName = info.ProviderDisplayName;
|
||||
ReturnUrl = returnUrl;
|
||||
return Page();
|
||||
}
|
||||
|
||||
private UserModel CreateUser()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Activator.CreateInstance<UserModel>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException($"Can't create an instance of '{nameof(UserModel)}'. " +
|
||||
$"Ensure that '{nameof(UserModel)}' is not an abstract class and has a parameterless constructor, or alternatively " +
|
||||
$"override the external login page in /Areas/Identity/Pages/Account/ExternalLogin.cshtml");
|
||||
}
|
||||
}
|
||||
|
||||
private IUserEmailStore<UserModel> GetEmailStore()
|
||||
{
|
||||
if (!_userManager.SupportsUserEmail)
|
||||
{
|
||||
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||
}
|
||||
return (IUserEmailStore<UserModel>)_userStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
26
Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ForgotPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Forgot your password?";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h2>Enter your email.</h2>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
85
Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs
Normal file
85
Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs
Normal file
@ -0,0 +1,85 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ForgotPasswordModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public ForgotPasswordModel(UserManager<UserModel> userManager, IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToPage("./ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please
|
||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ResetPassword",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", code },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
await _emailSender.SendEmailAsync(
|
||||
Input.Email,
|
||||
"Reset Password",
|
||||
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
return RedirectToPage("./ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
@page
|
||||
@model ForgotPasswordConfirmation
|
||||
@{
|
||||
ViewData["Title"] = "Forgot password confirmation";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p>
|
||||
Please check your email to reset your password.
|
||||
</p>
|
||||
@ -0,0 +1,25 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class ForgotPasswordConfirmation : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
10
Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
@ -0,0 +1,10 @@
|
||||
@page
|
||||
@model LockoutModel
|
||||
@{
|
||||
ViewData["Title"] = "Locked out";
|
||||
}
|
||||
|
||||
<header>
|
||||
<h1 class="text-danger">@ViewData["Title"]</h1>
|
||||
<p class="text-danger">This account has been locked out, please try again later.</p>
|
||||
</header>
|
||||
25
Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
25
Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class LockoutModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
188
Areas/Identity/Pages/Account/Login.cshtml
Normal file
188
Areas/Identity/Pages/Account/Login.cshtml
Normal file
@ -0,0 +1,188 @@
|
||||
@page
|
||||
@model LoginModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Log in";
|
||||
}
|
||||
|
||||
<div class="row" id="systemLogin">
|
||||
<div class="row">
|
||||
<h2><label class="col-md-2">Login Type</label></h2>
|
||||
<div class="btn-group col-md-4" role="group" aria-label="Login type">
|
||||
<input type="radio" class="btn-check" name="loginType" id="local-login" value="Local" v-model="loginType">
|
||||
<label class="btn btn-outline-primary" for="local-login">Local</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="loginType" id="ad-login" value="AD" v-model="loginType" checked>
|
||||
<label class="btn btn-outline-primary" for="ad-login">AD</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4" v-if="loginType == 'Local'">
|
||||
<form id="account" method="post">
|
||||
<h2>Use a local account to log in.</h2>
|
||||
<hr />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label">Email</label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
|
||||
<label asp-for="Input.Password" class="form-label">Password</label>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4" v-if="loginType == 'AD'">
|
||||
<form v-on:submit.prevent="ldapLogin" id="login" method="post">
|
||||
<h2>Use a AD account to log in.</h2>
|
||||
<hr />
|
||||
<div class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input v-model="ldapLoginInfo.username" id="ldapUsername" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label id="ldapEmailLabel" class="form-label">Windows Login</label>
|
||||
<span id="ldapEmailError" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input v-model="ldapLoginInfo.password" id="ldapPassword" class="form-control" type="password" autocomplete="current-password" aria-required="true" placeholder="password" />
|
||||
<label id="ldapPasswordLabel" class="form-label">Password</label>
|
||||
<span id="ldapPasswordError" class="text-danger"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button id="ldap-login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-2">
|
||||
<section>
|
||||
<h3>Use another service to log in.</h3>
|
||||
<hr />
|
||||
@{
|
||||
if ((Model.ExternalLogins?.Count ?? 0) == 0)
|
||||
{
|
||||
<div>
|
||||
<p>
|
||||
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
|
||||
about setting up this ASP.NET application to support logging in via external services</a>.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
|
||||
<div>
|
||||
<p>
|
||||
@foreach (var provider in Model.ExternalLogins!)
|
||||
{
|
||||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
|
||||
<script>
|
||||
|
||||
$(function () {
|
||||
app.mount('#systemLogin');
|
||||
|
||||
$('.closeModal').on('click', function () {
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
loginType: 'AD',
|
||||
ldapLoginInfo: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
async ldapLogin() {
|
||||
try {
|
||||
// Show the loading modal
|
||||
$('#loadingModal').modal('show');
|
||||
|
||||
// Perform the fetch request
|
||||
const response = await fetch('/IdentityAPI/LdapLogin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(this.ldapLoginInfo),
|
||||
});
|
||||
|
||||
// Check if the response is OK
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Redirect if a URL is provided
|
||||
if (data.redirectUrl) {
|
||||
window.location.href = data.redirectUrl;
|
||||
} else {
|
||||
console.error('Login failed:', data);
|
||||
alert('Login failed.');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error during LDAP login:', error);
|
||||
alert(error.message);
|
||||
}
|
||||
finally {
|
||||
await new Promise(resolve => {
|
||||
$('#loadingModal').on('shown.bs.modal', resolve);
|
||||
});
|
||||
$('#loadingModal').modal('hide');
|
||||
}
|
||||
},
|
||||
async fetchControllerMethodList() {
|
||||
try {
|
||||
const response = await fetch('/AdminAPI/GetListClassAndMethodInformation', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Assign data if it exists
|
||||
if (data) {
|
||||
this.controllerMethodData = data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}
|
||||
141
Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
141
Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
@ -0,0 +1,141 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LoginModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly ILogger<LoginModel> _logger;
|
||||
|
||||
public LoginModel(SignInManager<UserModel> signInManager, ILogger<LoginModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
public async Task OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ErrorMessage);
|
||||
}
|
||||
|
||||
returnUrl ??= Url.Content("~/");
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
||||
{
|
||||
returnUrl ??= Url.Content("~/");
|
||||
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
39
Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
@ -0,0 +1,39 @@
|
||||
@page
|
||||
@model LoginWith2faModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<hr />
|
||||
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post" asp-route-returnUrl="@Model.ReturnUrl">
|
||||
<input asp-for="RememberMe" type="hidden" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" />
|
||||
<label asp-for="Input.TwoFactorCode" class="form-label"></label>
|
||||
<span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="checkbox mb-3">
|
||||
<label asp-for="Input.RememberMachine" class="form-label">
|
||||
<input asp-for="Input.RememberMachine" />
|
||||
@Html.DisplayNameFor(m => m.Input.RememberMachine)
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Don't have access to your authenticator device? You can
|
||||
<a id="recovery-code-login" asp-page="./LoginWithRecoveryCode" asp-route-returnUrl="@Model.ReturnUrl">log in with a recovery code</a>.
|
||||
</p>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
132
Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
132
Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
@ -0,0 +1,132 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LoginWith2faModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<LoginWith2faModel> _logger;
|
||||
|
||||
public LoginWith2faModel(
|
||||
SignInManager<UserModel> signInManager,
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<LoginWith2faModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool RememberMe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Authenticator code")]
|
||||
public string TwoFactorCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
RememberMe = rememberMe;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine);
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml
Normal file
29
Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml
Normal file
@ -0,0 +1,29 @@
|
||||
@page
|
||||
@model LoginWithRecoveryCodeModel
|
||||
@{
|
||||
ViewData["Title"] = "Recovery code verification";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<hr />
|
||||
<p>
|
||||
You have requested to log in with a recovery code. This login will not be remembered until you provide
|
||||
an authenticator app code at log in or disable 2FA and log in again.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" placeholder="RecoveryCode" />
|
||||
<label asp-for="Input.RecoveryCode" class="form-label"></label>
|
||||
<span asp-validation-for="Input.RecoveryCode" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
113
Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs
Normal file
113
Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs
Normal file
@ -0,0 +1,113 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LoginWithRecoveryCodeModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<LoginWithRecoveryCodeModel> _logger;
|
||||
|
||||
public LoginWithRecoveryCodeModel(
|
||||
SignInManager<UserModel> signInManager,
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<LoginWithRecoveryCodeModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
[Required]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Recovery Code")]
|
||||
public string RecoveryCode { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id);
|
||||
return LocalRedirect(returnUrl ?? Url.Content("~/"));
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Areas/Identity/Pages/Account/Logout.cshtml
Normal file
21
Areas/Identity/Pages/Account/Logout.cshtml
Normal file
@ -0,0 +1,21 @@
|
||||
@page
|
||||
@model LogoutModel
|
||||
@{
|
||||
ViewData["Title"] = "Log out";
|
||||
}
|
||||
|
||||
<header>
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
@{
|
||||
if (User.Identity?.IsAuthenticated ?? false)
|
||||
{
|
||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
||||
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>You have successfully logged out of the application.</p>
|
||||
}
|
||||
}
|
||||
</header>
|
||||
43
Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
43
Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LogoutModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly ILogger<LogoutModel> _logger;
|
||||
|
||||
public LogoutModel(SignInManager<UserModel> signInManager, ILogger<LogoutModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost(string returnUrl = null)
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
_logger.LogInformation("User logged out.");
|
||||
if (returnUrl != null)
|
||||
{
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This needs to be a redirect so that the browser performs a new
|
||||
// request and the identity for the user gets updated.
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml
Normal file
36
Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml
Normal file
@ -0,0 +1,36 @@
|
||||
@page
|
||||
@model ChangePasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Change password";
|
||||
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="change-password-form" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.OldPassword" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your old password." />
|
||||
<label asp-for="Input.OldPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.OldPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your new password." />
|
||||
<label asp-for="Input.NewPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your new password."/>
|
||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
128
Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs
Normal file
128
Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs
Normal file
@ -0,0 +1,128 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ChangePasswordModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly ILogger<ChangePasswordModel> _logger;
|
||||
|
||||
public ChangePasswordModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager,
|
||||
ILogger<ChangePasswordModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Current password")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if (!hasPassword)
|
||||
{
|
||||
return RedirectToPage("./SetPassword");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
|
||||
if (!changePasswordResult.Succeeded)
|
||||
{
|
||||
foreach (var error in changePasswordResult.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
_logger.LogInformation("User changed their password successfully.");
|
||||
StatusMessage = "Your password has been changed.";
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
@page
|
||||
@model DeletePersonalDataModel
|
||||
@{
|
||||
ViewData["Title"] = "Delete Personal Data";
|
||||
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form id="delete-user" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
@if (Model.RequirePassword)
|
||||
{
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your password." />
|
||||
<label asp-for="Input.Password" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
<button class="w-100 btn btn-lg btn-danger" type="submit">Delete data and close my account</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
104
Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs
Normal file
104
Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs
Normal file
@ -0,0 +1,104 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class DeletePersonalDataModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly ILogger<DeletePersonalDataModel> _logger;
|
||||
|
||||
public DeletePersonalDataModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager,
|
||||
ILogger<DeletePersonalDataModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool RequirePassword { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
RequirePassword = await _userManager.HasPasswordAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
RequirePassword = await _userManager.HasPasswordAsync(user);
|
||||
if (RequirePassword)
|
||||
{
|
||||
if (!await _userManager.CheckPasswordAsync(user, Input.Password))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Incorrect password.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _userManager.DeleteAsync(user);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected error occurred deleting user.");
|
||||
}
|
||||
|
||||
await _signInManager.SignOutAsync();
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
|
||||
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml
Normal file
25
Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml
Normal file
@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model Disable2faModel
|
||||
@{
|
||||
ViewData["Title"] = "Disable two-factor authentication (2FA)";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<strong>This action only disables 2FA.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
|
||||
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form method="post">
|
||||
<button class="btn btn-danger" type="submit">Disable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
70
Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs
Normal file
70
Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs
Normal file
@ -0,0 +1,70 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class Disable2faModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<Disable2faModel> _logger;
|
||||
|
||||
public Disable2faModel(
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<Disable2faModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!await _userManager.GetTwoFactorEnabledAsync(user))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected error occurred disabling 2FA.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
|
||||
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
|
||||
return RedirectToPage("./TwoFactorAuthentication");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
@page
|
||||
@model DownloadPersonalDataModel
|
||||
@{
|
||||
ViewData["Title"] = "Download Your Data";
|
||||
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class DownloadPersonalDataModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<DownloadPersonalDataModel> _logger;
|
||||
|
||||
public DownloadPersonalDataModel(
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<DownloadPersonalDataModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User));
|
||||
|
||||
// Only include personal data for download
|
||||
var personalData = new Dictionary<string, string>();
|
||||
var personalDataProps = typeof(UserModel).GetProperties().Where(
|
||||
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
|
||||
foreach (var p in personalDataProps)
|
||||
{
|
||||
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
|
||||
}
|
||||
|
||||
var logins = await _userManager.GetLoginsAsync(user);
|
||||
foreach (var l in logins)
|
||||
{
|
||||
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
|
||||
}
|
||||
|
||||
personalData.Add($"Authenticator Key", await _userManager.GetAuthenticatorKeyAsync(user));
|
||||
|
||||
Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json");
|
||||
return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json");
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
44
Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
@ -0,0 +1,44 @@
|
||||
@page
|
||||
@model EmailModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage Email";
|
||||
ViewData["ActivePage"] = ManageNavPages.Email;
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="email-form" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
|
||||
@if (Model.IsEmailConfirmed)
|
||||
{
|
||||
<div class="form-floating mb-3 input-group">
|
||||
<input asp-for="Email" class="form-control" placeholder="Please enter your email." disabled />
|
||||
<div class="input-group-append">
|
||||
<span class="h-100 input-group-text text-success font-weight-bold">✓</span>
|
||||
</div>
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Email" class="form-control" placeholder="Please enter your email." disabled />
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
|
||||
</div>
|
||||
}
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" placeholder="Please enter new email." />
|
||||
<label asp-for="Input.NewEmail" class="form-label"></label>
|
||||
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<button id="change-email-button" type="submit" asp-page-handler="ChangeEmail" class="w-100 btn btn-lg btn-primary">Change email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
172
Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
172
Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
@ -0,0 +1,172 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class EmailModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public EmailModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool IsEmailConfirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "New email")]
|
||||
public string NewEmail { get; set; }
|
||||
}
|
||||
|
||||
private async Task LoadAsync(UserModel user)
|
||||
{
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
Email = email;
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
NewEmail = email,
|
||||
};
|
||||
|
||||
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostChangeEmailAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
if (Input.NewEmail != email)
|
||||
{
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmailChange",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, email = Input.NewEmail, code = code },
|
||||
protocol: Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(
|
||||
Input.NewEmail,
|
||||
"Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
StatusMessage = "Confirmation link to change email sent. Please check your email.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
StatusMessage = "Your email is unchanged.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = code },
|
||||
protocol: Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(
|
||||
email,
|
||||
"Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
StatusMessage = "Verification email sent. Please check your email.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
@page
|
||||
@model EnableAuthenticatorModel
|
||||
@{
|
||||
ViewData["Title"] = "Configure authenticator app";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div>
|
||||
<p>To use an authenticator app go through the following steps:</p>
|
||||
<ol class="list">
|
||||
<li>
|
||||
<p>
|
||||
Download a two-factor authenticator app like Microsoft Authenticator for
|
||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
|
||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or
|
||||
Google Authenticator for
|
||||
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">Android</a> and
|
||||
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
|
||||
with a unique code. Enter the code in the confirmation box below.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="send-code" method="post">
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Code" class="form-control" autocomplete="off" placeholder="Please enter the code."/>
|
||||
<label asp-for="Input.Code" class="control-label form-label">Verification Code</label>
|
||||
<span asp-validation-for="Input.Code" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Verify</button>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@ -0,0 +1,189 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class EnableAuthenticatorModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<EnableAuthenticatorModel> _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
|
||||
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
public EnableAuthenticatorModel(
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<EnableAuthenticatorModel> logger,
|
||||
UrlEncoder urlEncoder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string AuthenticatorUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Verification Code")]
|
||||
public string Code { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Strip spaces and hyphens
|
||||
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError("Input.Code", "Verification code is invalid.");
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
_logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
|
||||
|
||||
StatusMessage = "Your authenticator app has been verified.";
|
||||
|
||||
if (await _userManager.CountRecoveryCodesAsync(user) == 0)
|
||||
{
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
RecoveryCodes = recoveryCodes.ToArray();
|
||||
return RedirectToPage("./ShowRecoveryCodes");
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToPage("./TwoFactorAuthentication");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSharedKeyAndQrCodeUriAsync(UserModel user)
|
||||
{
|
||||
// Load the authenticator key & QR code URI to display on the form
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
SharedKey = FormatKey(unformattedKey);
|
||||
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey);
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
|
||||
currentPosition += 4;
|
||||
}
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.AsSpan(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
AuthenticatorUriFormat,
|
||||
_urlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml
Normal file
53
Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml
Normal file
@ -0,0 +1,53 @@
|
||||
@page
|
||||
@model ExternalLoginsModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage your external logins";
|
||||
ViewData["ActivePage"] = ManageNavPages.ExternalLogins;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
@if (Model.CurrentLogins?.Count > 0)
|
||||
{
|
||||
<h3>Registered Logins</h3>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@foreach (var login in Model.CurrentLogins)
|
||||
{
|
||||
<tr>
|
||||
<td id="@($"login-provider-{login.LoginProvider}")">@login.ProviderDisplayName</td>
|
||||
<td>
|
||||
@if (Model.ShowRemoveButton)
|
||||
{
|
||||
<form id="@($"remove-login-{login.LoginProvider}")" asp-page-handler="RemoveLogin" method="post">
|
||||
<div>
|
||||
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" />
|
||||
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" />
|
||||
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
@:
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
@if (Model.OtherLogins?.Count > 0)
|
||||
{
|
||||
<h4>Add another service to log in.</h4>
|
||||
<hr />
|
||||
<form id="link-login-form" asp-page-handler="LinkLogin" method="post" class="form-horizontal">
|
||||
<div id="socialLoginList">
|
||||
<p>
|
||||
@foreach (var provider in Model.OtherLogins)
|
||||
{
|
||||
<button id="@($"link-login-button-{provider.Name}")" type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
142
Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs
Normal file
142
Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs
Normal file
@ -0,0 +1,142 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ExternalLoginsModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly IUserStore<UserModel> _userStore;
|
||||
|
||||
public ExternalLoginsModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager,
|
||||
IUserStore<UserModel> userStore)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_userStore = userStore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<UserLoginInfo> CurrentLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<AuthenticationScheme> OtherLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool ShowRemoveButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
CurrentLogins = await _userManager.GetLoginsAsync(user);
|
||||
OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
|
||||
.Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
|
||||
.ToList();
|
||||
|
||||
string passwordHash = null;
|
||||
if (_userStore is IUserPasswordStore<UserModel> userPasswordStore)
|
||||
{
|
||||
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
ShowRemoveButton = passwordHash != null || CurrentLogins.Count > 1;
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostRemoveLoginAsync(string loginProvider, string providerKey)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
StatusMessage = "The external login was not removed.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "The external login was removed.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostLinkLoginAsync(string provider)
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
// Request a redirect to the external login provider to link a login for the current user
|
||||
var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback");
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
|
||||
return new ChallengeResult(provider, properties);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetLinkLoginCallbackAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync(userId);
|
||||
if (info == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected error occurred loading external login info.");
|
||||
}
|
||||
|
||||
var result = await _userManager.AddLoginAsync(user, info);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
StatusMessage = "The external login was not added. External logins can only be associated with one account.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
StatusMessage = "The external login was added.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
@page
|
||||
@model GenerateRecoveryCodesModel
|
||||
@{
|
||||
ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||
<strong>Put these codes in a safe place.</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
<p>
|
||||
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
|
||||
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<form method="post">
|
||||
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -0,0 +1,83 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class GenerateRecoveryCodesModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<GenerateRecoveryCodesModel> _logger;
|
||||
|
||||
public GenerateRecoveryCodesModel(
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<GenerateRecoveryCodesModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
if (!isTwoFactorEnabled)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot generate recovery codes for user because they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
if (!isTwoFactorEnabled)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot generate recovery codes for user as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
RecoveryCodes = recoveryCodes.ToArray();
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
|
||||
StatusMessage = "You have generated new recovery codes.";
|
||||
return RedirectToPage("./ShowRecoveryCodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Areas/Identity/Pages/Account/Manage/Index.cshtml
Normal file
30
Areas/Identity/Pages/Account/Manage/Index.cshtml
Normal file
@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "Profile";
|
||||
ViewData["ActivePage"] = ManageNavPages.Index;
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="profile-form" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Username" class="form-control" placeholder="Please choose your username." disabled />
|
||||
<label asp-for="Username" class="form-label"></label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number."/>
|
||||
<label asp-for="Input.PhoneNumber" class="form-label"></label>
|
||||
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
|
||||
</div>
|
||||
<button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
119
Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
Normal file
119
Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
Normal file
@ -0,0 +1,119 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
|
||||
public IndexModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Phone]
|
||||
[Display(Name = "Phone number")]
|
||||
public string PhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
private async Task LoadAsync(UserModel user)
|
||||
{
|
||||
var userName = await _userManager.GetUserNameAsync(user);
|
||||
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
|
||||
|
||||
Username = userName;
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
PhoneNumber = phoneNumber
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
|
||||
if (Input.PhoneNumber != phoneNumber)
|
||||
{
|
||||
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
|
||||
if (!setPhoneResult.Succeeded)
|
||||
{
|
||||
StatusMessage = "Unexpected error when trying to set phone number.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Your profile has been updated";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
Normal file
123
Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
Normal file
@ -0,0 +1,123 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static class ManageNavPages
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string Index => "Index";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string Email => "Email";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ChangePassword => "ChangePassword";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string DownloadPersonalData => "DownloadPersonalData";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string DeletePersonalData => "DeletePersonalData";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ExternalLogins => "ExternalLogins";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string PersonalData => "PersonalData";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
var activePage = viewContext.ViewData["ActivePage"] as string
|
||||
?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName);
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Areas/Identity/Pages/Account/Manage/PersonalData.cshtml
Normal file
27
Areas/Identity/Pages/Account/Manage/PersonalData.cshtml
Normal file
@ -0,0 +1,27 @@
|
||||
@page
|
||||
@model PersonalDataModel
|
||||
@{
|
||||
ViewData["Title"] = "Personal Data";
|
||||
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p>Your account contains personal data that you have given us. This page allows you to download or delete that data.</p>
|
||||
<p>
|
||||
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
||||
</p>
|
||||
<form id="download-data" asp-page="DownloadPersonalData" method="post">
|
||||
<button class="btn btn-primary" type="submit">Download</button>
|
||||
</form>
|
||||
<p>
|
||||
<a id="delete" asp-page="DeletePersonalData" class="btn btn-danger">Delete</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
37
Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs
Normal file
37
Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class PersonalDataModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly ILogger<PersonalDataModel> _logger;
|
||||
|
||||
public PersonalDataModel(
|
||||
UserManager<UserModel> userManager,
|
||||
ILogger<PersonalDataModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
@page
|
||||
@model ResetAuthenticatorModel
|
||||
@{
|
||||
ViewData["Title"] = "Reset authenticator key";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
|
||||
</p>
|
||||
<p>
|
||||
This process disables 2FA until you verify your authenticator app.
|
||||
If you do not complete your authenticator app configuration you may lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<form id="reset-authenticator-form" method="post">
|
||||
<button id="reset-authenticator-button" class="btn btn-danger" type="submit">Reset authenticator key</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -0,0 +1,68 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ResetAuthenticatorModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly ILogger<ResetAuthenticatorModel> _logger;
|
||||
|
||||
public ResetAuthenticatorModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager,
|
||||
ILogger<ResetAuthenticatorModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
|
||||
|
||||
return RedirectToPage("./EnableAuthenticator");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Areas/Identity/Pages/Account/Manage/SetPassword.cshtml
Normal file
35
Areas/Identity/Pages/Account/Manage/SetPassword.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model SetPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Set password";
|
||||
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||
}
|
||||
|
||||
<h3>Set your password</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<p class="text-info">
|
||||
You do not have a local username/password for this site. Add a local
|
||||
account so you can log in without an external login.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="set-password-form" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" placeholder="Please enter your new password."/>
|
||||
<label asp-for="Input.NewPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" placeholder="Please confirm your new password."/>
|
||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
115
Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs
Normal file
115
Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs
Normal file
@ -0,0 +1,115 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class SetPasswordModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
|
||||
public SetPasswordModel(
|
||||
UserManager<UserModel> userManager,
|
||||
SignInManager<UserModel> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
|
||||
if (hasPassword)
|
||||
{
|
||||
return RedirectToPage("./ChangePassword");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword);
|
||||
if (!addPasswordResult.Succeeded)
|
||||
{
|
||||
foreach (var error in addPasswordResult.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Your password has been set.";
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml
Normal file
25
Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml
Normal file
@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model ShowRecoveryCodesModel
|
||||
@{
|
||||
ViewData["Title"] = "Recovery codes";
|
||||
ViewData["ActivePage"] = "TwoFactorAuthentication";
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<strong>Put these codes in a safe place.</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
|
||||
{
|
||||
<code class="recovery-code">@Model.RecoveryCodes[row]</code><text> </text><code class="recovery-code">@Model.RecoveryCodes[row + 1]</code><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,47 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class ShowRecoveryCodesModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if (RecoveryCodes == null || RecoveryCodes.Length == 0)
|
||||
{
|
||||
return RedirectToPage("./TwoFactorAuthentication");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
@page
|
||||
@using Microsoft.AspNetCore.Http.Features
|
||||
@model TwoFactorAuthenticationModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication (2FA)";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
@{
|
||||
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
|
||||
@if (consentFeature?.CanTrack ?? true)
|
||||
{
|
||||
@if (Model.Is2faEnabled)
|
||||
{
|
||||
if (Model.RecoveryCodesLeft == 0)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have no recovery codes left.</strong>
|
||||
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have 1 recovery code left.</strong>
|
||||
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (Model.IsMachineRemembered)
|
||||
{
|
||||
<form method="post" style="display: inline-block">
|
||||
<button type="submit" class="btn btn-primary">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
|
||||
}
|
||||
|
||||
<h4>Authenticator app</h4>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
|
||||
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>Privacy and cookie policy have not been accepted.</strong>
|
||||
<p>You must accept the policy before you can enable two factor authentication.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class TwoFactorAuthenticationModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly ILogger<TwoFactorAuthenticationModel> _logger;
|
||||
|
||||
public TwoFactorAuthenticationModel(
|
||||
UserManager<UserModel> userManager, SignInManager<UserModel> signInManager, ILogger<TwoFactorAuthenticationModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool HasAuthenticator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public int RecoveryCodesLeft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public bool Is2faEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool IsMachineRemembered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null;
|
||||
Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user);
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _signInManager.ForgetTwoFactorClientAsync();
|
||||
StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
29
Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
@ -0,0 +1,29 @@
|
||||
@{
|
||||
if (ViewData.TryGetValue("ParentLayout", out var parentLayout) && parentLayout != null)
|
||||
{
|
||||
Layout = parentLayout.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Layout = "/Areas/Identity/Pages/_Layout.cshtml";
|
||||
}
|
||||
}
|
||||
|
||||
<h1>Manage your account</h1>
|
||||
|
||||
<div>
|
||||
<h2>Change your account settings</h2>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<partial name="_ManageNav" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@RenderSection("Scripts", required: false)
|
||||
}
|
||||
15
Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml
Normal file
15
Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml
Normal file
@ -0,0 +1,15 @@
|
||||
@inject SignInManager<UserModel> SignInManager
|
||||
@{
|
||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||
}
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
|
||||
@if (hasExternalLogins)
|
||||
{
|
||||
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
|
||||
}
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
|
||||
</ul>
|
||||
10
Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml
Normal file
10
Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml
Normal file
@ -0,0 +1,10 @@
|
||||
@model string
|
||||
|
||||
@if (!String.IsNullOrEmpty(Model))
|
||||
{
|
||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
@Model
|
||||
</div>
|
||||
}
|
||||
1
Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml
Normal file
1
Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml
Normal file
@ -0,0 +1 @@
|
||||
@using PSTW_CentralSystem.Areas.Identity.Pages.Account.Manage
|
||||
67
Areas/Identity/Pages/Account/Register.cshtml
Normal file
67
Areas/Identity/Pages/Account/Register.cshtml
Normal file
@ -0,0 +1,67 @@
|
||||
@page
|
||||
@model RegisterModel
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||
<h2>Create a new account.</h2>
|
||||
<hr />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email">Email</label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
|
||||
<label asp-for="Input.Password">Password</label>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
|
||||
<label asp-for="Input.ConfirmPassword">Confirm Password</label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-2">
|
||||
<section>
|
||||
<h3>Use another service to register.</h3>
|
||||
<hr />
|
||||
@{
|
||||
if ((Model.ExternalLogins?.Count ?? 0) == 0)
|
||||
{
|
||||
<div>
|
||||
<p>
|
||||
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
|
||||
about setting up this ASP.NET application to support logging in via external services</a>.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
|
||||
<div>
|
||||
<p>
|
||||
@foreach (var provider in Model.ExternalLogins!)
|
||||
{
|
||||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
182
Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
182
Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
@ -0,0 +1,182 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
[Authorize]
|
||||
public class RegisterModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<UserModel> _signInManager;
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly IUserStore<UserModel> _userStore;
|
||||
private readonly IUserEmailStore<UserModel> _emailStore;
|
||||
private readonly ILogger<RegisterModel> _logger;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public RegisterModel(
|
||||
UserManager<UserModel> userManager,
|
||||
IUserStore<UserModel> userStore,
|
||||
SignInManager<UserModel> signInManager,
|
||||
ILogger<RegisterModel> logger,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userStore = userStore;
|
||||
_emailStore = GetEmailStore();
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public async Task OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
ReturnUrl = returnUrl;
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
||||
{
|
||||
returnUrl ??= Url.Content("~/");
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = CreateUser();
|
||||
|
||||
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
|
||||
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
|
||||
var result = await _userManager.CreateAsync(user, Input.Password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
if (_userManager.Options.SignIn.RequireConfirmedAccount)
|
||||
{
|
||||
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
|
||||
}
|
||||
else
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
}
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return Page();
|
||||
}
|
||||
|
||||
private UserModel CreateUser()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Activator.CreateInstance<UserModel>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException($"Can't create an instance of '{nameof(UserModel)}'. " +
|
||||
$"Ensure that '{nameof(UserModel)}' is not an abstract class and has a parameterless constructor, or alternatively " +
|
||||
$"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
|
||||
}
|
||||
}
|
||||
|
||||
private IUserEmailStore<UserModel> GetEmailStore()
|
||||
{
|
||||
if (!_userManager.SupportsUserEmail)
|
||||
{
|
||||
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||
}
|
||||
return (IUserEmailStore<UserModel>)_userStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Areas/Identity/Pages/Account/RegisterConfirmation.cshtml
Normal file
23
Areas/Identity/Pages/Account/RegisterConfirmation.cshtml
Normal file
@ -0,0 +1,23 @@
|
||||
@page
|
||||
@model RegisterConfirmationModel
|
||||
@{
|
||||
ViewData["Title"] = "Register confirmation";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
@{
|
||||
if (@Model.DisplayConfirmAccountLink)
|
||||
{
|
||||
<p>
|
||||
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
|
||||
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your account</a>
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>
|
||||
Please check your email to confirm your account.
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
80
Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs
Normal file
80
Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs
Normal file
@ -0,0 +1,80 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class RegisterConfirmationModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly IEmailSender _sender;
|
||||
|
||||
public RegisterConfirmationModel(UserManager<UserModel> userManager, IEmailSender sender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool DisplayConfirmAccountLink { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string EmailConfirmationUrl { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
|
||||
{
|
||||
if (email == null)
|
||||
{
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with email '{email}'.");
|
||||
}
|
||||
|
||||
Email = email;
|
||||
// Once you add a real email sender, you should remove this code that lets you confirm the account
|
||||
DisplayConfirmAccountLink = true;
|
||||
if (DisplayConfirmAccountLink)
|
||||
{
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
EmailConfirmationUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
|
||||
protocol: Request.Scheme);
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
Normal file
26
Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
Normal file
@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ResendEmailConfirmationModel
|
||||
@{
|
||||
ViewData["Title"] = "Resend email confirmation";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h2>Enter your email.</h2>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Resend</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ResendEmailConfirmationModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public ResendEmailConfirmationModel(UserManager<UserModel> userManager, IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { userId = userId, code = code },
|
||||
protocol: Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(
|
||||
Input.Email,
|
||||
"Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
37
Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
@ -0,0 +1,37 @@
|
||||
@page
|
||||
@model ResetPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Reset password";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h2>Reset your password.</h2>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<input asp-for="Input.Code" type="hidden" />
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your password." />
|
||||
<label asp-for="Input.Password" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your password." />
|
||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
118
Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
Normal file
118
Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
Normal file
@ -0,0 +1,118 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ResetPasswordModel : PageModel
|
||||
{
|
||||
private readonly UserManager<UserModel> _userManager;
|
||||
|
||||
public ResetPasswordModel(UserManager<UserModel> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Code { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public IActionResult OnGet(string code = null)
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
return BadRequest("A code must be supplied for password reset.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))
|
||||
};
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToPage("./ResetPasswordConfirmation");
|
||||
}
|
||||
|
||||
var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToPage("./ResetPasswordConfirmation");
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
@page
|
||||
@model ResetPasswordConfirmationModel
|
||||
@{
|
||||
ViewData["Title"] = "Reset password confirmation";
|
||||
}
|
||||
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<p>
|
||||
Your password has been reset. Please <a asp-page="./Login">click here to log in</a>.
|
||||
</p>
|
||||
@ -0,0 +1,25 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class ResetPasswordConfirmationModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
10
Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
@ -0,0 +1,10 @@
|
||||
@model string
|
||||
|
||||
@if (!String.IsNullOrEmpty(Model))
|
||||
{
|
||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
@Model
|
||||
</div>
|
||||
}
|
||||
1
Areas/Identity/Pages/Account/_ViewImports.cshtml
Normal file
1
Areas/Identity/Pages/Account/_ViewImports.cshtml
Normal file
@ -0,0 +1 @@
|
||||
@using PSTW_CentralSystem.Areas.Identity.Pages.Account
|
||||
23
Areas/Identity/Pages/Error.cshtml
Normal file
23
Areas/Identity/Pages/Error.cshtml
Normal file
@ -0,0 +1,23 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
|
||||
</p>
|
||||
41
Areas/Identity/Pages/Error.cshtml.cs
Normal file
41
Areas/Identity/Pages/Error.cshtml.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Identity.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
Normal file
2
Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
Normal file
@ -0,0 +1,2 @@
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
5
Areas/Identity/Pages/_ViewImports.cshtml
Normal file
5
Areas/Identity/Pages/_ViewImports.cshtml
Normal file
@ -0,0 +1,5 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using PSTW_CentralSystem.Areas.Identity
|
||||
@using PSTW_CentralSystem.Areas.Identity.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using PSTW_CentralSystem.Models
|
||||
4
Areas/Identity/Pages/_ViewStart.cshtml
Normal file
4
Areas/Identity/Pages/_ViewStart.cshtml
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
@{
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Controllers.Admin
|
||||
{
|
||||
[Area("Inventory")]
|
||||
[Authorize(Policy = "RoleModulePolicy")]
|
||||
public class InventoryMasterController : Controller
|
||||
{
|
||||
public IActionResult AdminDashboard()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult ItemRegistration()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult ItemMovement()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult ItemRequestMaster()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult ProductRegistration()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult SupplierRegistration()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public IActionResult ManifacturerRegistration()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult StationRegistration()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public IActionResult QrMaster()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Areas/Inventory/Controllers/ItemMovementController.cs
Normal file
45
Areas/Inventory/Controllers/ItemMovementController.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PSTW_CentralSystem.Areas.Inventory.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Controllers
|
||||
{
|
||||
[Area("Inventory")]
|
||||
|
||||
[Authorize(Policy = "RoleModulePolicy")]
|
||||
public class ItemMovementController : Controller
|
||||
{
|
||||
// GET: Inventory
|
||||
public ActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public ActionResult ItemMovementUser()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public ActionResult QrUser()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult ItemRequest()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult UserDashboard()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("/i/{id}")]
|
||||
public IActionResult ItemRecognization(string id, [FromBody] ItemModel item)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
18
Areas/Inventory/Controllers/MainController.cs
Normal file
18
Areas/Inventory/Controllers/MainController.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Controllers
|
||||
{
|
||||
[Area("Inventory")]
|
||||
//[Authorize(Policy = "RoleModulePolicy")]
|
||||
public class MainController : Controller
|
||||
{
|
||||
// GET: Inventory
|
||||
public ActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
20
Areas/Inventory/Models/InventoryMasterModel.cs
Normal file
20
Areas/Inventory/Models/InventoryMasterModel.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PSTW_CentralSystem.Models;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class InventoryMasterModel
|
||||
{
|
||||
[Key]
|
||||
public int StoreId { get; set; }
|
||||
public int UserId { get; set; }
|
||||
[ForeignKey("UserId")]
|
||||
public virtual UserModel? User { get; set; }
|
||||
[ForeignKey("StoreId")]
|
||||
public virtual StoreModel? Store { get; set; }
|
||||
}
|
||||
}
|
||||
50
Areas/Inventory/Models/ItemModel.cs
Normal file
50
Areas/Inventory/Models/ItemModel.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PSTW_CentralSystem.Models;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class ItemModel
|
||||
{
|
||||
[Key]
|
||||
public int ItemID { get; set; }
|
||||
public string UniqueID { get; set; } = string.Empty;
|
||||
public required int CompanyId { get; set; }
|
||||
public required int DepartmentId { get; set; }
|
||||
public required int ProductId { get; set; }
|
||||
public required string? SerialNumber { get; set; }
|
||||
public required string? TeamType { get; set; }
|
||||
public required int Quantity { get; set; }
|
||||
public required string Supplier { get; set; }
|
||||
public required DateTime PurchaseDate { get; set; }
|
||||
public required string PONo { get; set; }
|
||||
public required string Currency { get; set; }
|
||||
public required float DefaultPrice { get; set; }
|
||||
public required float CurrencyRate { get; set; }
|
||||
public required float ConvertPrice { get; set; }
|
||||
public string? DONo { get; set; }
|
||||
public DateTime? DODate { get; set; }
|
||||
public required int Warranty { get; set; }
|
||||
public required DateTime EndWDate { get; set; }
|
||||
public string? InvoiceNo { get; set; }
|
||||
public DateTime? InvoiceDate { get; set; }
|
||||
[Comment("1 = In stock; 2 = Item Moving; 3 = Item Out; 4 = Item Broken; 5 = Item Lost; 6 = Item Stolen; 7 = Item Damaged; 8 = Item Discarded; 9 = Item Destroyed; 10 = Item Finished;")]
|
||||
public int ItemStatus { get; set; } = 1;
|
||||
public int? MovementId { get; set; }
|
||||
public string PartNumber { get; set; } = string.Empty;
|
||||
public int CreatedByUserId { get; set; }
|
||||
[ForeignKey("CreatedByUserId")]
|
||||
public virtual UserModel? CreatedBy { get; set; }
|
||||
[ForeignKey("CompanyId")]
|
||||
public virtual CompanyModel? Company { get; set; }
|
||||
[ForeignKey("DepartmentId")]
|
||||
public virtual DepartmentModel? Department { get; set; }
|
||||
[ForeignKey("ProductId")]
|
||||
public virtual ProductModel? Product { get; set; }
|
||||
[ForeignKey("MovementId")]
|
||||
public virtual ItemMovementModel? Movement { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
48
Areas/Inventory/Models/ItemMovementModel.cs
Normal file
48
Areas/Inventory/Models/ItemMovementModel.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using PSTW_CentralSystem.Models;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class ItemMovementModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
public int? ItemId { get; set; }
|
||||
public int? ToStation { get; set; }
|
||||
public int? ToStore { get; set; }
|
||||
public int? ToUser { get; set; }
|
||||
[Comment("Repair, Calibration, Faulty, Ready To Deploy, On Delivery")]
|
||||
public string? ToOther { get; set; }
|
||||
public DateTime? sendDate { get; set; }
|
||||
[Comment("Register, StockIn, Stock Out")]
|
||||
public string? Action { get; set; }
|
||||
public int? Quantity { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
public string? ConsignmentNote { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public int? LastUser { get; set; }
|
||||
public int? LastStore{ get; set; }
|
||||
public int? LastStation{ get; set; }
|
||||
[Comment("Repair, Calibration, Faulty, Ready To Deploy, On Delivery")]
|
||||
public string? LatestStatus { get; set; }
|
||||
public DateTime? receiveDate { get; set; }
|
||||
public bool MovementComplete { get; set; } = false;
|
||||
//public virtual ItemModel? Item { get; set; }
|
||||
//[ForeignKey("ToStore")]
|
||||
[ForeignKey("ItemId")]
|
||||
public virtual ItemModel? Item { get; set; }
|
||||
[ForeignKey("ToStore")]
|
||||
public virtual StoreModel? NextStore { get; set; }
|
||||
[ForeignKey("ToStation")]
|
||||
public virtual StationModel? NextStation { get; set; }
|
||||
[ForeignKey("ToUser")]
|
||||
public virtual UserModel? NextUser { get; set; }
|
||||
[ForeignKey("LastStore")]
|
||||
public virtual StoreModel? FromStore { get; set; }
|
||||
[ForeignKey("LastStation")]
|
||||
public virtual StationModel? FromStation { get; set; }
|
||||
[ForeignKey("LastUser")]
|
||||
public virtual UserModel? FromUser { get; set; }
|
||||
}
|
||||
}
|
||||
11
Areas/Inventory/Models/ManufacturerModel.cs
Normal file
11
Areas/Inventory/Models/ManufacturerModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class ManufacturerModel
|
||||
{
|
||||
[Key]
|
||||
public int ManufacturerId { get; set; }
|
||||
public required string ManufacturerName { get; set; }
|
||||
}
|
||||
}
|
||||
21
Areas/Inventory/Models/ProductModel.cs
Normal file
21
Areas/Inventory/Models/ProductModel.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using PSTW_CentralSystem.Models;
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class ProductModel
|
||||
{
|
||||
[Key]
|
||||
public int ProductId { get; set; }
|
||||
public required string ProductName { get; set; }
|
||||
public required string ProductShortName { get; set; }
|
||||
public required int ManufacturerId { get; set; }
|
||||
public required string Category { get; set; }
|
||||
public required string ModelNo { get; set; }
|
||||
public int? QuantityProduct { get; set; }
|
||||
public required string ImageProduct { get; set; }
|
||||
[ForeignKey("ManufacturerId")]
|
||||
public virtual ManufacturerModel? Manufacturer { get; set; }
|
||||
public virtual ICollection<ItemModel>? Items { get; set; } // Navigation property>
|
||||
}
|
||||
}
|
||||
31
Areas/Inventory/Models/RequestModel.cs
Normal file
31
Areas/Inventory/Models/RequestModel.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using PSTW_CentralSystem.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
[Table("request")]
|
||||
public class RequestModel
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int requestID { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
[ForeignKey("ProductId")]
|
||||
public virtual ProductModel? Product { get; set; }
|
||||
public int? StationId { get; set; }
|
||||
[ForeignKey("StationId")]
|
||||
public virtual StationModel? Station { get; set; }
|
||||
public int UserId { get; set; }
|
||||
[ForeignKey("UserId")]
|
||||
public virtual UserModel? User { get; set; }
|
||||
public string? ProductCategory { get; set; }
|
||||
public string? remarkUser { get; set; }
|
||||
public string? remarkMasterInv { get; set; }
|
||||
public string? status { get; set; }
|
||||
public DateTime requestDate { get; set; }
|
||||
public DateTime? approvalDate { get; set; }
|
||||
public int? RequestQuantity { get; set; }
|
||||
public string? Document { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
19
Areas/Inventory/Models/StationModel.cs
Normal file
19
Areas/Inventory/Models/StationModel.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using PSTW_CentralSystem.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class StationModel
|
||||
{
|
||||
[Key]
|
||||
public int StationId { get; set; }
|
||||
public int StationPicID { get; set; }
|
||||
public string? StationName { get; set; }
|
||||
public int DepartmentId { get; set; }
|
||||
[ForeignKey("DepartmentId")]
|
||||
public virtual DepartmentModel? Department { get; set; }
|
||||
[ForeignKey("StationPicID")]
|
||||
public virtual UserModel? StationPic { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
16
Areas/Inventory/Models/StoreModel.cs
Normal file
16
Areas/Inventory/Models/StoreModel.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using PSTW_CentralSystem.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class StoreModel
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
public int CompanyId { get; set; }
|
||||
public string StoreName { get; set; } = string.Empty;
|
||||
[ForeignKey("CompanyId")]
|
||||
public virtual CompanyModel? Company { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
19
Areas/Inventory/Models/SupplierModel.cs
Normal file
19
Areas/Inventory/Models/SupplierModel.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace PSTW_CentralSystem.Areas.Inventory.Models
|
||||
{
|
||||
public class SupplierModel
|
||||
{
|
||||
[Key]
|
||||
public int SupplierId { get; set; }
|
||||
public required string SupplierCompName { get; set; }
|
||||
public required string SupplierAddress { get; set; }
|
||||
[AllowNull]
|
||||
public string? SupplierPIC { get; set; }
|
||||
[AllowNull]
|
||||
public string? SupplierEmail { get; set; }
|
||||
[AllowNull]
|
||||
public string? SupplierPhoneNo { get; set; }
|
||||
}
|
||||
}
|
||||
116
Areas/Inventory/Views/InventoryMaster/AdminDashboard.cshtml
Normal file
116
Areas/Inventory/Views/InventoryMaster/AdminDashboard.cshtml
Normal file
@ -0,0 +1,116 @@
|
||||
@{
|
||||
ViewData["Title"] = "Dashboard";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
<div class="container" id="invAdmin">
|
||||
<div class="row">
|
||||
<div class="text-center">
|
||||
<p><h1 class="display-4">Inventory Admin Dashboard</h1></p>
|
||||
<p v-show="currentUserCompanyDept.departmentName"><h2 class="display-6">Store: {{ currentUserCompanyDept.departmentName }}</h2></p>
|
||||
</div>
|
||||
</div>
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml")
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Inventory Report</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="reportData">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-3">
|
||||
<h4>Statistic</h4>
|
||||
<p>Total Number of Item Registered: {{ reportData.itemCountRegistered }}</p>
|
||||
<p>Total Number of Item Still in Stock: {{ reportData.itemCountStillInStock }}</p>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h4>Item Registered </h4>
|
||||
<p>This Month: {{ reportData.itemCountRegisteredThisMonth }}</p>
|
||||
<p>Last Month: {{ reportData.itemCountRegisteredLastMonth }}</p>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h4>Item Stock Out </h4>
|
||||
<p>This Month: {{ reportData.itemCountStockOutThisMonth }}</p>
|
||||
<p>Last Month: {{ reportData.itemCountStockOutLastMonth }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#invAdmin');
|
||||
|
||||
$('.closeModal').on('click', function () {
|
||||
// Show the modal with the ID 'addManufacturerModal'.
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
currentUser: null,
|
||||
currentUserCompanyDept: {
|
||||
departmentName: null,
|
||||
departmentId: null
|
||||
},
|
||||
reportData: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUser();
|
||||
},
|
||||
methods: {
|
||||
async fetchUser() {
|
||||
try {
|
||||
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentUser = data?.userInfo || null;
|
||||
const companyDeptData = await this.currentUser.department;
|
||||
const userRole = this.currentUser.role;
|
||||
if(userRole == "SuperAdmin" || userRole == "SystemAdmin"){
|
||||
this.currentUserCompanyDept = {departmentId : 0, departmentName : "All"}
|
||||
this.fetchInventoryReport(0);
|
||||
}
|
||||
else{
|
||||
this.currentUserCompanyDept = companyDeptData;
|
||||
this.fetchInventoryReport(companyDeptData.departmentId);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
async fetchInventoryReport(deptId){
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/GetInventoryReport/` + deptId, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.reportData = data;
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}
|
||||
1174
Areas/Inventory/Views/InventoryMaster/ItemMovement.cshtml
Normal file
1174
Areas/Inventory/Views/InventoryMaster/ItemMovement.cshtml
Normal file
File diff suppressed because it is too large
Load Diff
1026
Areas/Inventory/Views/InventoryMaster/ItemRegistration.cshtml
Normal file
1026
Areas/Inventory/Views/InventoryMaster/ItemRegistration.cshtml
Normal file
File diff suppressed because it is too large
Load Diff
947
Areas/Inventory/Views/InventoryMaster/ItemRequestMaster.cshtml
Normal file
947
Areas/Inventory/Views/InventoryMaster/ItemRequestMaster.cshtml
Normal file
@ -0,0 +1,947 @@
|
||||
@{
|
||||
ViewData["Title"] = "Product Request";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml");
|
||||
<div id="requestProduct" class="row">
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h2>Pending Request</h2>
|
||||
@* <button id="addRequestBtn" class="btn btn-success col-md-3 col-lg-3 m-1 col-12"><i class="fa fa-plus"></i> Add Request</button> *@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* <div v-if="loading">
|
||||
<div class="spinner-border text-info" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div> *@
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="requestDatatable" style=" width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h2>Complete Requestt</h2>
|
||||
@* <button id="addRequestBtn" class="btn btn-success col-md-3 col-lg-3 m-1 col-12"><i class="fa fa-plus"></i> Add Request</button> *@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* <div v-if="loading">
|
||||
<div class="spinner-border text-info" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div> *@
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="settledrequestDatatable" style=" width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-labelledby="rejectRequestModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="rejectRequestModalLabel">Reject Request</h5>
|
||||
<button type="button" class="closeModal" data-dismiss="modal" aria-label="Close" v-on:click="showRequestModal=false">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<form v-on:submit.prevent="rejectRequest" data-aos="fade-right">
|
||||
@* <div class=" register" data-aos="fade-right"> *@
|
||||
<div data-aos="fade-right">
|
||||
<div class="row" data-aos="fade-right">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<h3 class="register-heading">REJECT REQUEST</h3>
|
||||
<div class="row register-form">
|
||||
<div class="col-md-12">
|
||||
|
||||
<div class="form-group row">
|
||||
@* <label class="col-sm-4 col-form-label hidden-label">Request Id</label> *@
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<input type="text" id="currentrequestID" name="currentrequestID" v-model="currentrequestID" class="form-control" hidden />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Remark</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<input type="text" id="rejectremark" name="rejectremark" v-model="rejectremark" class="form-control" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Submit and Reset Buttons *@
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-8 offset-sm-3">
|
||||
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
|
||||
<button type="submit" class="btn btn-primary m-1 submit-button">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-labelledby="approveRequestModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="approveRequestModalLabel">Approve Request</h5>
|
||||
<button type="button" class="closeModal" data-dismiss="modal" aria-label="Close" v-on:click="showRequestModal=false">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<form v-on:submit.prevent="approveRequest" data-aos="fade-right">
|
||||
@* <div class=" register" data-aos="fade-right"> *@
|
||||
<div data-aos="fade-right">
|
||||
<div class="row" data-aos="fade-right">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<h3 class="register-heading">APPROVE REQUEST</h3>
|
||||
<div class="row register-form">
|
||||
<div class="col-md-12">
|
||||
|
||||
<div class="form-group row">
|
||||
@* <label class="col-sm-4 col-form-label hidden-label">Request Id</label> *@
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<input type="text" id="currentrequestID" name="currentrequestID" v-model="currentrequestID" class="form-control" hidden />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Remark</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<input type="text" id="approveremark" name="approveremark" v-model="approveremark" class="form-control" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Submit and Reset Buttons *@
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-8 offset-sm-3">
|
||||
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
|
||||
<button type="submit" class="btn btn-primary m-1 submit-button">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#requestProduct');
|
||||
|
||||
// Attach a click event listener to elements with the class 'btn-success'.
|
||||
$('#addRequestBtn').on('click', function () {
|
||||
// Show the modal
|
||||
$('#requestModal').modal('show');
|
||||
});
|
||||
$('.closeModal').on('click', function () {
|
||||
// Show the modal
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
$('.submit-button').on('click', function () {
|
||||
// Show the modal
|
||||
$('#rejectModal').modal('hide');
|
||||
$('#approveModal').modal('hide');
|
||||
});
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
requestID : null,
|
||||
userId : null,
|
||||
stationId : null,
|
||||
productId : null,
|
||||
remark: "",
|
||||
document: null,
|
||||
quantity: 0,
|
||||
status: "",
|
||||
requestDate : null,
|
||||
approvalDate : null,
|
||||
productCategory: "",
|
||||
productName: null,
|
||||
productCategory: null,
|
||||
stations: [],
|
||||
selectedProduct: "",
|
||||
selectedStation: "",
|
||||
selectedCategory: "",
|
||||
showRequestModal: false,
|
||||
showRejectModal: false,
|
||||
showApproveModal: false,
|
||||
loading: false,
|
||||
request: [],
|
||||
currentUser: null,
|
||||
currentrequestID: "",
|
||||
rejectremark: "",
|
||||
approveremark: "",
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchRequest();
|
||||
this.fetchUser();
|
||||
this.fetchProducts();
|
||||
this.fetchStation();
|
||||
},
|
||||
computed: {
|
||||
// filteredDepartments() {
|
||||
// if (!this.selectedCompany) {
|
||||
// return []; No company selected, return empty list
|
||||
// }
|
||||
// const company = this.companies.find(c => c.companyId === this.selectedCompany);
|
||||
// this.selectedDepartment = '';
|
||||
// return company ? company.departments : [];
|
||||
// },
|
||||
// showProduct() {
|
||||
// if (!this.selectedProduct) {
|
||||
// return []; No company selected, return empty list
|
||||
// }
|
||||
// const product = this.products.find(c => c.productId === this.selectedProduct);
|
||||
// return product ? product : {};
|
||||
// },
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.rejectremark = "";
|
||||
},
|
||||
async addRequest() {
|
||||
try {
|
||||
const requiredFields = ['stationId', 'productId', 'quantity', 'productCategory'];
|
||||
|
||||
// Loop through required fields and check if any are null or empty
|
||||
for (let field of requiredFields) {
|
||||
if (this[field] === null || this[field] === '') {
|
||||
alert('Request Error', `Please fill in required fields: ${field}.`, 'warning');
|
||||
return; // Exit early if validation fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Proceed to send the data to the API
|
||||
const response = await fetch('/InvMainAPI/AddRequest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
if (response.ok) {
|
||||
// If the form submission was successful, display a success message
|
||||
alert('Success!', 'Request form has been successfully submitted.', 'success');
|
||||
const requestItem = await response.json();
|
||||
this.items.push(requestItem);
|
||||
|
||||
this.fetchRequest();
|
||||
|
||||
} else {
|
||||
throw new Error('Failed to submit form.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
|
||||
// Displaying error message
|
||||
alert('Inventory PSTW Error', `An error occurred: ${error.message}`, 'error');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
initiateTable() {
|
||||
self = this;
|
||||
this.requestDatatable = $('#requestDatatable').DataTable({
|
||||
"data": this.items.filter(item => item.status == "Requested"),
|
||||
"columns": [
|
||||
{
|
||||
"title": "Request ID",
|
||||
"data": "requestID",
|
||||
},
|
||||
{
|
||||
"title": "Action",
|
||||
"data" :"requestID",
|
||||
"render": function (data, type, row) {
|
||||
var actiontButtons = `<div class="row" style="padding: 5px;"> <button type="button" class="btn btn-success approve-btn" data-id="${data}">Approve</button></div> <div class="row" style="padding: 5px;"><button type="button" class="btn btn-danger reject-btn" data-id="${data}">Reject</button></div>`;
|
||||
return actiontButtons
|
||||
},
|
||||
"className": "align-middle",
|
||||
},
|
||||
{
|
||||
"title": "Product",
|
||||
"data": "productName",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
var imageSrc = full.productImage;
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(imageSrc);
|
||||
var isPdf = /\.pdf$/i.test(imageSrc);
|
||||
// var imageSrc = full.productImage; Fallback to data if imgsrc is unavailable
|
||||
console.log(full);
|
||||
|
||||
if (isImage) {
|
||||
return ` <div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${imageSrc}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
}
|
||||
// else {
|
||||
// return `<a href="${imageSrc}" target="_blank">Download File</a>`;
|
||||
// }
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Requested by User",
|
||||
"data": "userName",
|
||||
},
|
||||
{
|
||||
"title": "Requested by Station",
|
||||
"data": "stationName",
|
||||
},
|
||||
{
|
||||
"title": "Product Category",
|
||||
"data": "productCategory",
|
||||
},
|
||||
{
|
||||
"title": "Request Quantity",
|
||||
"data": "requestQuantity",
|
||||
},
|
||||
{
|
||||
"title": "Document/Picture",
|
||||
"data": "document",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
|
||||
var isPdf = /\.pdf$/i.test(data);
|
||||
|
||||
if (isImage) {
|
||||
return `<a href="${data}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<a href="${data}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
} else {
|
||||
return `<a href="${data}" target="_blank">Download File</a>`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "User Remark",
|
||||
"data": "remarkUser",
|
||||
},
|
||||
// {
|
||||
// "title": "InvMaster Remark",
|
||||
// "data": "remarkMasterInv",
|
||||
// },
|
||||
{
|
||||
"title": "Status",
|
||||
"data": "status",
|
||||
},
|
||||
{
|
||||
"title": "Request Date",
|
||||
"data": "requestDate",
|
||||
},
|
||||
{
|
||||
"title": "Approval Date",
|
||||
"data": "approvalDate",
|
||||
},
|
||||
// {
|
||||
// "title": "Reject",
|
||||
// "data" :"requestID",
|
||||
// "render": function (data, type, row) {
|
||||
// var rejectButton = `<button type="button" class="btn btn-danger reject-btn" data-id="${data}">Reject</button>`;
|
||||
// return rejectButton
|
||||
// },
|
||||
// "className": "align-middle",
|
||||
// },
|
||||
// {
|
||||
// "title": "Approve",
|
||||
// "data": "requestID",
|
||||
// "render": function (data) {
|
||||
// var approveButton = `<button type="button" class="btn btn-success approve-btn" data-id="${data}">Approve</button>`;
|
||||
// return approveButton;
|
||||
// },
|
||||
// "className": "align-middle",
|
||||
// }
|
||||
],
|
||||
responsive: true,
|
||||
drawCallback: function (settings) {
|
||||
// Generate QR codes after rows are rendered
|
||||
// const api = this.api();
|
||||
// api.rows().every(function () {
|
||||
// const data = this.data(); Row data
|
||||
// const containerId = `qr${data.requestID}`;
|
||||
// const container = $(`#${containerId}`);
|
||||
// container.empty();
|
||||
// container.append(`${data.requestID}`);
|
||||
// console.log(container[0]);
|
||||
// if (container) {
|
||||
// Generate QR code only if not already generated
|
||||
// new QRCode(container[0], {
|
||||
// text: data.qrString,
|
||||
// width: 100,
|
||||
// height: 100,
|
||||
// colorDark: "#000000",
|
||||
// colorLight: "#ffffff",
|
||||
// correctLevel: QRCode.CorrectLevel.M
|
||||
// });
|
||||
// }
|
||||
// container.on('click', function() {
|
||||
// window.open(data.qrString, '_blank');
|
||||
// });
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
||||
this.settledrequestDatatable = $('#settledrequestDatatable').DataTable({
|
||||
"data": this.items.filter(item => item.status !== "Requested"),
|
||||
"columns": [
|
||||
{
|
||||
"title": "Request ID",
|
||||
"data": "requestID",
|
||||
},
|
||||
{
|
||||
"title": "Product",
|
||||
"data": "productName",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
var imageSrc = full.productImage;
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(imageSrc);
|
||||
var isPdf = /\.pdf$/i.test(imageSrc);
|
||||
// var imageSrc = full.productImage; Fallback to data if imgsrc is unavailable
|
||||
|
||||
if (isImage) {
|
||||
return ` <div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${imageSrc}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
}
|
||||
// else {
|
||||
// return `<a href="${imageSrc}" target="_blank">Download File</a>`;
|
||||
// }
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Requested by User",
|
||||
"data": "userName",
|
||||
},
|
||||
{
|
||||
"title": "Requested by Station",
|
||||
"data": "stationName",
|
||||
},
|
||||
{
|
||||
"title": "Product Category",
|
||||
"data": "productCategory",
|
||||
},
|
||||
{
|
||||
"title": "Request Quantity",
|
||||
"data": "requestQuantity",
|
||||
},
|
||||
{
|
||||
"title": "Document/Picture",
|
||||
"data": "document",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
|
||||
var isPdf = /\.pdf$/i.test(data);
|
||||
|
||||
if (isImage) {
|
||||
return `<a href="${data}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<a href="${data}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
} else {
|
||||
return `<a href="${data}" target="_blank">Download File</a>`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "User Remark",
|
||||
"data": "remarkUser",
|
||||
},
|
||||
{
|
||||
"title": "InvMaster Remark",
|
||||
"data": "remarkMasterInv",
|
||||
},
|
||||
{
|
||||
"title": "Status",
|
||||
"data": "status",
|
||||
},
|
||||
{
|
||||
"title": "Request Date",
|
||||
"data": "requestDate",
|
||||
},
|
||||
{
|
||||
"title": "Approval Date",
|
||||
"data": "approvalDate",
|
||||
},
|
||||
// {
|
||||
// "title": "Reject",
|
||||
// "data" :null,
|
||||
// "render": function (data, type, row) {
|
||||
// return `<button type="button" class="btn btn-danger reject-btn"
|
||||
// data-id="${row.requestID}"
|
||||
// data-remark="${row.remark || ''}">
|
||||
// Reject
|
||||
// </button>`;
|
||||
// },
|
||||
// "className": "align-middle",
|
||||
// },
|
||||
// {
|
||||
// "title": "Approve",
|
||||
// "data": "requestID",
|
||||
// "render": function (data) {
|
||||
// var approveButton = `<button type="button" class="btn btn-success approve-btn" data-id="${data}">Approve</button>`;
|
||||
// return approveButton;
|
||||
// },
|
||||
// "className": "align-middle",
|
||||
// }
|
||||
],
|
||||
responsive: true,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Attach click event listener to the delete buttons
|
||||
$('#requestDatatable tbody').on('click', '.reject-btn', function () {
|
||||
const requestID = $(this).data('id');
|
||||
self.rejectRequestModal(requestID);
|
||||
});
|
||||
|
||||
$('#requestDatatable tbody').on('click', '.approve-btn', function () {
|
||||
const requestID = $(this).data('id');
|
||||
self.approveRequestModal(requestID);
|
||||
});
|
||||
|
||||
$('#requestDatatable tbody').on('click', '.print-btn', function () {
|
||||
const $button = $(this); // The clicked button
|
||||
const $row = $button.closest('tr'); // The parent row of the button
|
||||
const itemId = $button.data('id'); // Get the item ID from the button's data attribute
|
||||
|
||||
let imageSrc;
|
||||
|
||||
// Check if the table is collapsed
|
||||
if ($row.hasClass('child')) {
|
||||
// For collapsed view: Look for the closest `.dtr-data` that contains the img
|
||||
imageSrc = $row.prev('tr').find('td:nth-child(1) img').attr('src');
|
||||
} else {
|
||||
// For expanded view: Find the img in the first column of the current row
|
||||
imageSrc = $row.find('td:nth-child(1) img').attr('src');
|
||||
}
|
||||
|
||||
if (imageSrc) {
|
||||
self.printItem(itemId, imageSrc); // Call the print function with the itemId and imageSrc
|
||||
} else {
|
||||
console.error("Image source not found.");
|
||||
}
|
||||
});
|
||||
|
||||
$('#itemDatatable tbody').on('click', '.reject-btn', function () {
|
||||
const $button = $(this); // The clicked button
|
||||
const $row = $button.closest('tr'); // The parent row of the button
|
||||
const itemId = $button.data('id'); // Get the item ID from the button's data attribute
|
||||
|
||||
|
||||
self.printItem(itemId, imageSrc); // Call the print function with the itemId and imageSrc
|
||||
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async fetchRequest() {
|
||||
try {
|
||||
// const token = localStorage.getItem('token'); // Get the token from localStorage
|
||||
const response = await fetch('/InvMainAPI/ItemRequestList', {
|
||||
method: 'GET', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Set content type
|
||||
// 'Authorization': `Bearer ${token}` // Include the token in the headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch item');
|
||||
}
|
||||
this.items = await response.json();
|
||||
|
||||
if (this.requestDatatable) {
|
||||
this.requestDatatable.clear().destroy();
|
||||
}
|
||||
this.initiateTable();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching item:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchProducts() {
|
||||
try {
|
||||
// const token = localStorage.getItem('token'); // Get the token from localStorage
|
||||
const response = await fetch('/InvMainAPI/ProductList', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Set content type
|
||||
// 'Authorization': `Bearer ${token}` // Include the token in the headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch products');
|
||||
}
|
||||
|
||||
this.products = await response.json();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching products:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchStation() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/StationList', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch suppliers');
|
||||
}
|
||||
this.stations = await response.json(); // Get the full response object
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
}
|
||||
},
|
||||
async fetchStore() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/StoreList/', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch suppliers');
|
||||
}
|
||||
this.stores = await response.json(); // Get the full response object
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
}
|
||||
},
|
||||
async fetchUsers() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/UserList', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch suppliers');
|
||||
}
|
||||
this.users = await response.json(); // Get the full response object
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// FRONT END FUNCTIONS
|
||||
//----------------------//
|
||||
//Calculate Total Price
|
||||
convertCurrency() {
|
||||
const total = this.DefaultPrice * this.currencyRate;
|
||||
this.convertPrice = total.toFixed(2);
|
||||
this.DefaultPrice = this.DefaultPrice
|
||||
// .replace(/[^0-9.]/g, '') // Remove non-numeric characters except decimal points
|
||||
// .replace(/(\..*)\..*/g, '$1') // Allow only one decimal point
|
||||
// .replace(/^(\d*\.\d{0,2})\d*$/, '$1'); // Limit to two decimal places
|
||||
|
||||
},
|
||||
calculateWarrantyEndDate() {
|
||||
// Check if DODate and warranty are valid
|
||||
if (!this.DODate || isNaN(Date.parse(this.DODate))) {
|
||||
this.EndWDate = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const DODates = new Date(this.DODate);
|
||||
const warrantyMonth = parseInt(this.warranty);
|
||||
|
||||
// Ensure warranty is a valid number
|
||||
if (!isNaN(warrantyMonth)) {
|
||||
DODates.setMonth(DODates.getMonth() + warrantyMonth);
|
||||
this.EndWDate = DODates.toISOString().split('T')[0];
|
||||
} else {
|
||||
this.EndWDate = null;
|
||||
}
|
||||
},
|
||||
async approveRequest() {
|
||||
// if (!confirm("Are you sure you want to approve this request?")) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const formData = {
|
||||
RemarkMasterInv: this.rejectremark,
|
||||
|
||||
};
|
||||
|
||||
let requestID = this.currentrequestID;
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/ApproveRequest/${requestID}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
|
||||
//static update
|
||||
const row = $(`.approve-btn[data-id="${requestID}"]`).closest('tr');
|
||||
let rowData = this.requestDatatable.row(row).data();
|
||||
|
||||
// Update the status and remark
|
||||
rowData.status = "Approved";
|
||||
|
||||
// Remove row from requestDatatable
|
||||
this.requestDatatable.row(row).remove().draw();
|
||||
|
||||
// Add updated row to settledrequestDatatable
|
||||
this.settledrequestDatatable.row.add(rowData).draw();
|
||||
|
||||
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error approving request:", error);
|
||||
// alert("An error occurred while approving the request.");
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async rejectRequest() {
|
||||
|
||||
const formData = {
|
||||
RemarkMasterInv: this.rejectremark,
|
||||
|
||||
};
|
||||
|
||||
let requestID = this.currentrequestID;
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch(`/InvMainAPI/RejectRequest/${requestID}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
|
||||
});
|
||||
if (response.ok) {
|
||||
// If the form submission was successful, display a success message
|
||||
// alert('Success!', 'Request has been Rejected.', 'success');
|
||||
|
||||
const row = $(`.approve-btn[data-id="${requestID}"]`).closest('tr');
|
||||
let rowData = this.requestDatatable.row(row).data();
|
||||
|
||||
// Update the status and remark
|
||||
rowData.status = "Rejected";
|
||||
rowData.remarkMasterInv = this.rejectremark;
|
||||
|
||||
// Remove row from requestDatatable
|
||||
this.requestDatatable.row(row).remove().draw();
|
||||
|
||||
// Add updated row to settledrequestDatatable
|
||||
this.settledrequestDatatable.row.add(rowData).draw();
|
||||
|
||||
|
||||
} else {
|
||||
throw new Error('Failed to submit form.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
|
||||
// Displaying error message
|
||||
alert('Inventory PSTW Error', `An error occurred: ${error.message}`, 'error');
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async rejectRequestModal(requestID, remark) {
|
||||
|
||||
this.currentrequestID = requestID;
|
||||
this.rejectremark = remark;
|
||||
|
||||
|
||||
$(`#rejectModal`).modal('show');
|
||||
|
||||
|
||||
},
|
||||
async approveRequestModal(requestID, remark) {
|
||||
|
||||
this.currentrequestID = requestID;
|
||||
this.approveremark = remark;
|
||||
|
||||
|
||||
$(`#approveModal`).modal('show');
|
||||
|
||||
|
||||
},
|
||||
async printItem(itemId, imgSrc) {
|
||||
try {
|
||||
this.thisQRInfo.uniqueID = itemId;
|
||||
const uniqueQR = itemId;
|
||||
const container = document.getElementById("QrContainer");
|
||||
|
||||
if (!container) {
|
||||
console.error("Container not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Safely set image content
|
||||
const sanitizedImgSrc = encodeURI(imgSrc); // Sanitize the URL
|
||||
container.innerHTML = `<img src="${sanitizedImgSrc}" alt="QR Code" class="text-center " />`;
|
||||
|
||||
// Fetch QR information
|
||||
const qrInfo = this.getPrintedQR(uniqueQR);
|
||||
if (!qrInfo) {
|
||||
console.error("QR Info not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.thisQRInfo = qrInfo;
|
||||
this.thisQRInfo.imgSrc = sanitizedImgSrc
|
||||
this.thisQRInfo.imgContainer = container.innerHTML
|
||||
$(`#QrItemModal`).modal('show'); // Show modal
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error generating QR code:", error);
|
||||
alert("An error occurred while generating the QR code.");
|
||||
}
|
||||
},
|
||||
async fetchUser() {
|
||||
try {
|
||||
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentUser = data?.userInfo || null;
|
||||
const companyDeptData = await this.currentUser.department;
|
||||
this.currentUserCompanyDept = companyDeptData;
|
||||
this.selectedCompany = companyDeptData?.companyId || "";
|
||||
this.selectedDepartment = companyDeptData?.departmentId || "";
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
@{
|
||||
ViewData["Title"] = "Manufactures";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml");
|
||||
<div id="app">
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<button id="addManufacturerBtn" class="btn btn-success col-md-3 m-1"><i class="fa fa-plus"></i> Add Manufacturer</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="loading">
|
||||
<div class="spinner-border text-info" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="manufacturerTable" style="width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="addManufacturerModal" tabindex="-1" role="dialog" aria-labelledby="addManufacturerModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addManufacturerModalLabel">Add Manufacturer</h5>
|
||||
<button type="button" class="closeModal" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form v-on:submit.prevent="addManufacturer">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="manufacturerName">Manufacturer Name:</label>
|
||||
<input type="text" class="form-control" id="manufacturerName" v-model="newManufacturer.manufacturerName" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary closeModal" data-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
manufacturer: null,
|
||||
manufacturerDatatable: null,
|
||||
newManufacturer: {
|
||||
manufacturerName: null,
|
||||
},
|
||||
loading: true,
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
// Fetch companies, depts, and products from the API
|
||||
this.fetchManufactures();
|
||||
this.initiateTable();
|
||||
},
|
||||
methods: {
|
||||
async fetchManufactures() {
|
||||
fetch('/InvMainAPI/ManufacturerList', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data != null && data.length > 0)
|
||||
{
|
||||
this.manufacturer = data;
|
||||
}
|
||||
if (!this.manufacturerDatatable) {
|
||||
this.initiateTable();
|
||||
} else {
|
||||
this.fillTable(data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
});
|
||||
|
||||
},
|
||||
async initiateTable() {
|
||||
this.manufacturerDatatable = $('#manufacturerTable').DataTable({
|
||||
"data": this.manufacturer,
|
||||
"columns": [
|
||||
{ "title": "Manufacturer Name",
|
||||
"data": "manufacturerName",
|
||||
},
|
||||
{ "title": "Delete",
|
||||
"data": "manufacturerName",
|
||||
"render": function (data, type, full, meta) {
|
||||
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${full.manufacturerId}">Delete</button>`;
|
||||
return deleteButton;
|
||||
},
|
||||
"width": '10%',
|
||||
},
|
||||
],
|
||||
})
|
||||
self = this;
|
||||
// Attach click event listener to the delete buttons
|
||||
$('#manufacturerTable tbody').on('click', '.delete-btn', function () {
|
||||
const manufacturerId = $(this).data('id'); // Get the manufacturer ID from the button
|
||||
self.deleteManufacturer(manufacturerId); // Call the Vue method
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
fillTable(data){
|
||||
if (!this.manufacturerDatatable) {
|
||||
console.error("DataTable not initialized");
|
||||
return;
|
||||
}
|
||||
this.manufacturerDatatable.clear();
|
||||
this.manufacturerDatatable.rows.add(data);
|
||||
this.manufacturerDatatable.draw();
|
||||
this.loading = false;
|
||||
},
|
||||
addManufacturer() {
|
||||
this.loading = true;
|
||||
const existingManufacturer = this.manufacturer != null ? this.manufacturer.find(m => m.manufacturerName.toLowerCase() === this.newManufacturer.manufacturerName.toLowerCase()) : null;
|
||||
if (existingManufacturer) {
|
||||
alert('Manufacturer already exists');
|
||||
return;
|
||||
}
|
||||
fetch('/InvMainAPI/AddManufacturer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.newManufacturer)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data != null && data.length > 0)
|
||||
{
|
||||
this.manufacturer = data;
|
||||
}
|
||||
this.fillTable(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
$('#preloader').modal('hide');
|
||||
this.newManufacturer.manufacturerName = null;
|
||||
});
|
||||
},
|
||||
async deleteManufacturer(manufacturerId) {
|
||||
if (!confirm("Are you sure you want to delete this manufacturer?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/DeleteManufacturer/${manufacturerId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
// Remove the row from DataTables
|
||||
this.manufacturerDatatable
|
||||
.row($(`.delete-btn[data-id="${manufacturerId}"]`).closest('tr'))
|
||||
.remove()
|
||||
.draw();
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error deleting manufacturer:", error);
|
||||
alert("An error occurred while deleting the manufacturer.");
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$(function () {
|
||||
app.mount('#app');
|
||||
|
||||
// Attach a click event listener to elements with the class 'btn-success'.
|
||||
$('#addManufacturerBtn').on('click', function () {
|
||||
// Show the modal with the ID 'addManufacturerModal'.
|
||||
$('#addManufacturerModal').modal('show');
|
||||
});
|
||||
$('.closeModal').on('click', function () {
|
||||
// Show the modal with the ID 'addManufacturerModal'.
|
||||
$('#addManufacturerModal').modal('hide');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
1074
Areas/Inventory/Views/InventoryMaster/OtRegister.cshtml
Normal file
1074
Areas/Inventory/Views/InventoryMaster/OtRegister.cshtml
Normal file
File diff suppressed because it is too large
Load Diff
377
Areas/Inventory/Views/InventoryMaster/ProductRegistration.cshtml
Normal file
377
Areas/Inventory/Views/InventoryMaster/ProductRegistration.cshtml
Normal file
@ -0,0 +1,377 @@
|
||||
@{
|
||||
ViewData["Title"] = "Product Form";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
string userId = ViewBag.UserId;
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml");
|
||||
<div class="row">
|
||||
<div id="registerProduct" class="card m-1">
|
||||
<div class="row" v-if="addSection == true">
|
||||
<form v-on:submit.prevent="addProduct" data-aos="fade-right" >
|
||||
<div class="container register" data-aos="fade-right">
|
||||
<div class="row" data-aos="fade-right">
|
||||
<div class="col-md-12">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<div class="card-header">
|
||||
<h3 class="register-heading">REGISTRATION PRODUCT</h3>
|
||||
</div>
|
||||
<div class="row register-form card-body">
|
||||
<div class="col-md-6">
|
||||
|
||||
@* Product Name *@
|
||||
<div class="form-group row">
|
||||
<label for="productName" class="col-sm-3">Product Name:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="productName" name="productName" class="form-control" required v-model="productName">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Product Short Name *@
|
||||
<div class="form-group row">
|
||||
<label for="productName" class="col-sm-3">Product Short Name:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="productShortName" name="productShortName" class="form-control" maxlength="13" v-model="productShortName" required>
|
||||
<p><em><small class="text-danger">* Product short name limited to 13 characters</small></em></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Manufacturer *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3">Manufacturer:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="">
|
||||
<select class="btn btn-primary form-select" v-model="manufacturer" required>
|
||||
<option class="btn-light" value="" selected disabled>Select Manufacturer</option>
|
||||
<option class="btn-light" v-for="(item, index) in manufacturers" :key="item.manufacturerId" :value="item.manufacturerId">{{ item.manufacturerName ?? 'Select Manufacturer' }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Category *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3">Category:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="">
|
||||
<select class="btn btn-primary form-select" v-model="category" required>
|
||||
<option class="btn-light" value="" selected disabled>Select Category</option>
|
||||
<option class="btn-light" v-for="(item, index) in categories" :key="item" :value="item">{{ item ?? 'Select Category' }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
||||
@* Model No Coding *@
|
||||
<div class="form-group row">
|
||||
<label for="modelNo" class="col-sm-3">Model No:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="modelNo" name="modelNo" class="form-control" required v-model="modelNo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Image Product Coding *@
|
||||
<div class="form-group row">
|
||||
<label for="imageProduct" class="col-sm-3">Image:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="file" id="imageProduct" name="imageProduct" class="form-control" v-on:change="previewImage" accept="image/*" required>
|
||||
<br>
|
||||
<img v-if="imageSrc" :src="imageSrc" alt="Image Preview" class="img-thumbnail" style="width: 200px; margin-top: 10px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<button type="button" v-on:click="resetForm" class="btn btn-secondary mx-1">Reset</button>
|
||||
<input type="submit" class="btn btn-primary mx-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row">
|
||||
<button class="btn btn-danger col-md-3 m-1" v-on:click="addSection = false"><i class="fa fa-minus"></i> Hide Add Product Section</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="addProductBtn" class="btn btn-success col-md-3 m-1" v-show="addSection == false" v-on:click="addSection = true"><i class="fa fa-plus"></i> Show Add Product Section</button>
|
||||
</div>
|
||||
<div class="row table-responsive">
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="productDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#registerProduct');
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
addSection: false,
|
||||
productName: null,
|
||||
manufacturer: '',
|
||||
manufacturers: null,
|
||||
category: '',
|
||||
categories: ["Asset", "Part", "Disposable"],
|
||||
modelNo: null,
|
||||
imageProduct: null,
|
||||
manufactures: [],
|
||||
showOtherManufacturer: false,
|
||||
imageSrc: '',
|
||||
products: null,
|
||||
productDatatable: null,
|
||||
productShortName: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Fetch companies, depts, and products from the API
|
||||
this.fetchManufactures();
|
||||
this.fetchProducts();
|
||||
},
|
||||
methods: {
|
||||
initiateTable() {
|
||||
console.log(this.products)
|
||||
this.productDatatable = $('#productDatatable').DataTable({
|
||||
"data": this.products,
|
||||
"columns": [
|
||||
{ "title": "Product Name",
|
||||
"data": "productName",
|
||||
},
|
||||
{ "title": "Product Short Name",
|
||||
"data": "productShortName",
|
||||
},
|
||||
{ "title": "Model Number",
|
||||
"data": "modelNo",
|
||||
},
|
||||
{ "title": "Manufacturer",
|
||||
"data": "manufacturer.manufacturerName",
|
||||
},
|
||||
{ "title": "Product Category",
|
||||
"data": "category",
|
||||
},
|
||||
{ "title": "Product Stock",
|
||||
"data": "quantityProduct",
|
||||
},
|
||||
{ "title": "Image",
|
||||
"data": "imageProduct",
|
||||
"render": function (data, type, full, meta) {
|
||||
var image = `<a href="${data}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
return image;
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Delete",
|
||||
"data": "productId",
|
||||
"render": function (data) {
|
||||
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data.productId}">Delete</button>`;
|
||||
return deleteButton;
|
||||
},
|
||||
}
|
||||
|
||||
],
|
||||
responsive:true,
|
||||
})
|
||||
self = this;
|
||||
// Attach click event listener to the delete buttons
|
||||
$('#productDatatable tbody').on('click', '.delete-btn', function () {
|
||||
const productId = $(this).data('id'); // Get the manufacturer ID from the button
|
||||
self.deleteProduct(productId); // Call the Vue method
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
async fetchManufactures() {
|
||||
fetch('/InvMainAPI/ManufacturerList', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data != null && data.length > 0)
|
||||
{
|
||||
this.manufacturers = data;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
});
|
||||
},
|
||||
async fetchProducts() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/ProductList',{
|
||||
method: 'POST'
|
||||
}); // Call the API
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch products');
|
||||
}
|
||||
this.products = await response.json(); // Store the fetched products
|
||||
this.$nextTick(() => {
|
||||
this.initiateTable()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching products:', error);
|
||||
}
|
||||
},
|
||||
async addProduct() {
|
||||
// const existingProduct = this.products.find(p => p.modelNo === this.modelNo);
|
||||
// if (existingProduct) {
|
||||
// alert(`Product Error: The model number ${this.modelNo} already exists.`, 'error');
|
||||
// return; // Exit early if the modelNo exists
|
||||
// }
|
||||
|
||||
// Create the payload
|
||||
const formData = {
|
||||
productName: this.productName,
|
||||
productShortName: this.productShortName,
|
||||
manufacturerId: this.manufacturer,
|
||||
category: this.category,
|
||||
modelNo: this.modelNo,
|
||||
imageProduct: this.imageProduct
|
||||
};
|
||||
|
||||
try {
|
||||
// List of required fields
|
||||
const requiredFields = ['productName', 'manufacturer', 'category', 'modelNo', 'imageProduct'];
|
||||
|
||||
// Loop through required fields and check if any are null or empty
|
||||
for (let field of requiredFields) {
|
||||
if (this[field] === null || this[field] === '') {
|
||||
alert('Product Error', `Please fill in required fields: ${field}.`, 'warning');
|
||||
return; // Exit early if validation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed to send the data as raw JSON string
|
||||
const response = await fetch('/InvMainAPI/AddProduct', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData) // Convert the formData to a JSON string
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
|
||||
} else {
|
||||
this.products = await response.json();
|
||||
alert('Success!', 'Product form has been successfully submitted.', 'success');
|
||||
this.fillTable(this.products);
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Product Error', `An error occurred: ${error.message}`, 'error');
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.productName = null;
|
||||
this.manufacturer = '';
|
||||
this.category = '';
|
||||
this.modelNo = null;
|
||||
this.imageProduct = null;
|
||||
this.imageSrc = '';
|
||||
const fileInput = document.getElementById('imageProduct');
|
||||
if (fileInput) {
|
||||
fileInput.value = ''; // Clear the file input value
|
||||
}
|
||||
},
|
||||
|
||||
// Update Select View
|
||||
updateManufacturer(manufacturer) {
|
||||
this.manufacturer = manufacturer;
|
||||
this.showOtherManufacturer = false;
|
||||
},
|
||||
updateCategory(category) {
|
||||
this.category = category;
|
||||
},
|
||||
|
||||
// When User Presses Button Other
|
||||
toggleOtherInput(type) {
|
||||
if (type === 'manufacturer') {
|
||||
this.showOtherManufacturer = true;
|
||||
}
|
||||
},
|
||||
|
||||
// User Inserting an Image
|
||||
previewImage(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imageSrc = e.target.result; // Show the image preview
|
||||
this.imageProduct = e.target.result.split(',')[1]; // Get Base64 string (remove metadata)
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
this.imageSrc = '';
|
||||
this.imageProduct = null;
|
||||
}
|
||||
},
|
||||
async deleteProduct(productId) {
|
||||
if (!confirm("Are you sure you want to delete this product?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/DeleteProduct/${productId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
// Remove the row from DataTables
|
||||
this.productDatatable
|
||||
.row($(`.delete-btn[data-id="${productId}"]`).closest('tr'))
|
||||
.remove()
|
||||
.draw();
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error deleting product:", error);
|
||||
alert("An error occurred while deleting the product.");
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
fillTable(data){
|
||||
if (!this.productDatatable) {
|
||||
console.error("DataTable not initialized");
|
||||
return;
|
||||
}
|
||||
this.productDatatable.clear();
|
||||
this.productDatatable.rows.add(data);
|
||||
this.productDatatable.draw();
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
1073
Areas/Inventory/Views/InventoryMaster/QrMaster.cshtml
Normal file
1073
Areas/Inventory/Views/InventoryMaster/QrMaster.cshtml
Normal file
File diff suppressed because it is too large
Load Diff
298
Areas/Inventory/Views/InventoryMaster/StationRegistration.cshtml
Normal file
298
Areas/Inventory/Views/InventoryMaster/StationRegistration.cshtml
Normal file
@ -0,0 +1,298 @@
|
||||
@{
|
||||
ViewData["Title"] = "Station";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml");
|
||||
<div id="registerStation">
|
||||
<form v-on:submit.prevent="addStation" data-aos="fade-right" id="registerStationForm" v-if="registerStationForm">
|
||||
<div class="container register" data-aos="fade-right">
|
||||
<div class="row">
|
||||
<div class="col-md-9 register-rights" data-aos="fade-right">
|
||||
<div class="tab-content" id="myTabContent" data-aos="fade-right">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab" data-aos="fade-right">
|
||||
<h3 class="register-heading">REGISTRATION STATION</h3>
|
||||
<div class="row register-form">
|
||||
<div class="col-md-61">
|
||||
|
||||
@* Station Name *@
|
||||
<div class="form-group row">
|
||||
<label for="stationName" class="col-sm-3">Station Name:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="stationName" name="stationName" class="form-control" required v-model="stationName">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* User ID *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Station User PIC</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<select class="btn btn-primary dropdown-toggle col-md-10" v-model="selectedUserName" :disabled="currentUser != null" required v-on:change="updateDepartment()">
|
||||
<option class="btn-light" value="" disabled selected>Select User</option>
|
||||
<option v-for="(technicianUser, index) in users" :key="index" :value="technicianUser.fullname">
|
||||
{{ technicianUser.fullname }}
|
||||
</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Department ID *@
|
||||
<!-- Department Dropdown -->
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Department: </label>
|
||||
<div class="col-sm-8">
|
||||
<!-- Use a span to keep the same space occupied -->
|
||||
<span v-if="selectedDepartment">{{ selectedDepartment }}</span>
|
||||
<span v-else>No department assigned</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
|
||||
<button type="submit" class="btn btn-primary m-1">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<button id="addStationBtn" :class="['btn', 'col-md-3', 'col-lg-3', 'm-1', 'col-12', registerStationForm ? 'btn-danger' : 'btn-success']" v-on:click="registerStationForm = !registerStationForm"><i :class="['fa', registerStationForm ? 'fa-minus' : 'fa-plus']"></i> {{registerStationForm ? 'Hide Add Station' : 'Show Add Station'}}</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="stationDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#registerStation');
|
||||
|
||||
// Attach a click event listener to elements with the class 'btn-success'.
|
||||
$('#addStationBtn').on('click', function () {
|
||||
// Show the modal with the ID 'addStationModal'.
|
||||
$('#registerStationModal').modal('show');
|
||||
});
|
||||
$('.closeModal').on('click', function () {
|
||||
// Show the modal with the ID 'addStationModal'.
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
stationName: null,
|
||||
stationUserPIC: null,
|
||||
departmentId : null,
|
||||
selectedUserName : null,
|
||||
selectedDepartment: null,
|
||||
stations: null,
|
||||
currentUser: null,
|
||||
users : null,
|
||||
registerStationForm: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUsers();
|
||||
this.fetchStations();
|
||||
},
|
||||
methods: {
|
||||
async fetchUsers() {
|
||||
try {
|
||||
const response = await fetch(`/IdentityAPI/GetTechnicianUserInformation/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.users = data.technicianUsers;
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
async fetchStations() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/StationList', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
return;
|
||||
}
|
||||
if (this.stationDatatable) {
|
||||
this.stationDatatable.clear().destroy();
|
||||
}
|
||||
const stations = await response.json();
|
||||
this.stations = stations;
|
||||
|
||||
if ($.fn.dataTable.isDataTable('#stationDatatable')) {
|
||||
$('#stationDatatable').DataTable().clear().destroy();
|
||||
}
|
||||
this.initiateTable();
|
||||
} catch (error) {
|
||||
console.error('Error fetching stations:', error);
|
||||
this.errorMessage = 'Error: ' + error.message;
|
||||
}
|
||||
},
|
||||
async addStation() {
|
||||
$('#loadingModal').modal('show');
|
||||
// Create the payload
|
||||
const formData = {
|
||||
StationName: this.stationName,
|
||||
StationPicID: this.stationUserPIC,
|
||||
DepartmentId: this.departmentId,
|
||||
};
|
||||
|
||||
try {
|
||||
// List of required fields
|
||||
const requiredFields = ['stationName', 'stationUserPIC', 'departmentId'];
|
||||
|
||||
// Loop through required fields and check if any are null or empty
|
||||
for (let field of requiredFields) {
|
||||
if (this[field] === null || this[field] === '') {
|
||||
alert('Station Error', `Please fill in required fields: ${field}.`, 'warning');
|
||||
return; // Exit early if validation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed to send the data as raw JSON string
|
||||
const response = await fetch('/InvMainAPI/AddStation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData) // Convert the formData to a JSON string
|
||||
});
|
||||
|
||||
await this.fetchStations();
|
||||
$('#loadingModal').modal('hide');
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
|
||||
} else {
|
||||
this.fetchStations();
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Station Error', `An error occurred: ${error.message}`, 'error');
|
||||
}
|
||||
finally {
|
||||
await new Promise(resolve => {
|
||||
$('#loadingModal').on('shown.bs.modal', resolve);
|
||||
});
|
||||
$('#loadingModal').modal('hide');
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.selectedUserName = null;
|
||||
this.selectedDepartment = null;
|
||||
this.stationName = null;
|
||||
this.stationUserPIC = null;
|
||||
this.departmentId = '';
|
||||
},
|
||||
initiateTable() {
|
||||
self = this;
|
||||
this.stationDatatable = $('#stationDatatable').DataTable({
|
||||
"data": this.stations,
|
||||
"columns": [
|
||||
{
|
||||
"title": "Station Name",
|
||||
"data": "stationName",
|
||||
},
|
||||
{
|
||||
"title": "Station User PIC",
|
||||
"data": "stationPicID",
|
||||
},
|
||||
{
|
||||
"title": "Department ID",
|
||||
"data": "departmentId",
|
||||
},
|
||||
{
|
||||
"title": "Delete",
|
||||
"data": "stationId",
|
||||
"render": function (data) {
|
||||
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
|
||||
return deleteButton;
|
||||
},
|
||||
}
|
||||
],
|
||||
responsive: true,
|
||||
|
||||
})
|
||||
|
||||
// Attach click event listener to the delete buttons
|
||||
$('#stationDatatable tbody').on('click', '.delete-btn', function () {
|
||||
const stationId = $(this).data('id');
|
||||
self.deleteStation(stationId);
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
updateDepartment() {
|
||||
// Find the selected user by their full name
|
||||
const selectedUser = this.users.find(user => user.fullname === this.selectedUserName);
|
||||
if (selectedUser) {
|
||||
this.stationUserPIC = selectedUser.id;
|
||||
this.departmentId = selectedUser.department.departmentId;
|
||||
this.selectedDepartment = selectedUser.department.departmentName; // Set department name
|
||||
}
|
||||
},
|
||||
async deleteStation(stationId) {
|
||||
if (!confirm("Are you sure you want to delete this station?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/DeleteStation/${stationId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ stationId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fetchStations();
|
||||
} catch (error) {
|
||||
console.error('Error deleting stations:', error);
|
||||
this.errorMessage = 'Error: ' + error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@ -0,0 +1,287 @@
|
||||
@{
|
||||
ViewData["Title"] = "User Form";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartial.cshtml");
|
||||
<div id="registerSupplier">
|
||||
<form v-on:submit.prevent="addSupplier" data-aos="fade-right" id="registerSupplierForm" v-if="registerSupplierForm">
|
||||
<div class="container register" data-aos="fade-right">
|
||||
<div class="row">
|
||||
@*Right Side*@
|
||||
<div class="col-md-9 register-rights" data-aos="fade-right">
|
||||
<div class="tab-content" id="myTabContent" data-aos="fade-right">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab" data-aos="fade-right">
|
||||
<h3 class="register-heading">REGISTRATION SUPPLIER</h3>
|
||||
<div class="row register-form">
|
||||
<div class="col-md-61">
|
||||
|
||||
@* Supplier Name *@
|
||||
<div class="form-group row">
|
||||
<label for="supplierCompName" class="col-sm-3">Supplier Company Name:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="supplierCompName" name="supplierCompName" class="form-control" required v-model="supplierCompName">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Supplier Gender *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3">Supplier Address:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="dropdown">
|
||||
<textarea type="text" id="supplierAddress" name="supplierAddress" class="form-control" required v-model="supplierAddress"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Supplier PIC *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3">Supplier PIC:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="email" id="supplierPIC" name="supplierPIC" class="form-control" v-model="supplierPIC">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Supplier Email *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3">Supplier Email:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="email" id="supplierEmail" name="supplierEmail" class="form-control" v-model="supplierEmail">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Supplier Number Phone *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3">Supplier Phone Number:</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="tel" id="supplierPhoneNo" name="supplierPhoneNo" class="form-control" v-model="supplierPhoneNo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
|
||||
<button type="submit" class="btn btn-primary m-1">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<button id="addSupplierBtn" :class="['btn', 'col-md-3', 'col-lg-3', 'm-1', 'col-12', registerSupplierForm ? 'btn-danger' : 'btn-success']" v-on:click="registerSupplierForm = !registerSupplierForm"><i :class="['fa', registerSupplierForm ? 'fa-minus' : 'fa-plus']"></i> {{registerSupplierForm ? 'Hide Add Supplier' : 'Show Add Supplier'}}</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="supplierDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#registerSupplier');
|
||||
|
||||
// Attach a click event listener to elements with the class 'btn-success'.
|
||||
$('#addSupplierBtn').on('click', function () {
|
||||
// Show the modal with the ID 'addManufacturerModal'.
|
||||
$('#registerSupplierModal').modal('show');
|
||||
});
|
||||
$('.closeModal').on('click', function () {
|
||||
// Show the modal with the ID 'addManufacturerModal'.
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
supplierCompName : null,
|
||||
supplierEmail : null,
|
||||
supplierAddress : null,
|
||||
supplierPhoneNo : null,
|
||||
supplierPIC : null,
|
||||
suppliers: null,
|
||||
supplierDatatable: null,
|
||||
gender: ["Male", "Female", "Helicopter"],
|
||||
registerSupplierForm: false
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
this.fetchSuppliers();
|
||||
},
|
||||
methods: {
|
||||
async fetchSuppliers() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/SupplierList', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
return;
|
||||
}
|
||||
if (this.supplierDatatable){
|
||||
this.supplierDatatable.clear().destroy();
|
||||
}
|
||||
const suppliers = await response.json();
|
||||
this.suppliers = suppliers;
|
||||
|
||||
if (this.itemDatatable) {
|
||||
this.itemDatatable.clear().destroy();
|
||||
}
|
||||
this.initiateTable();
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
this.errorMessage = 'Error: ' + error.message;
|
||||
}
|
||||
},
|
||||
async addSupplier() {
|
||||
$('#loadingModal').modal('show');
|
||||
// Create the payload
|
||||
const formData = {
|
||||
supplierCompName: this.supplierCompName,
|
||||
supplierAddress: this.supplierAddress,
|
||||
supplierPIC: this.supplierPIC,
|
||||
supplierEmail: this.supplierEmail,
|
||||
supplierPhoneNo: this.supplierPhoneNo,
|
||||
};
|
||||
|
||||
try {
|
||||
// List of required fields
|
||||
const requiredFields = ['supplierCompName', 'supplierAddress'];
|
||||
|
||||
// Loop through required fields and check if any are null or empty
|
||||
for (let field of requiredFields) {
|
||||
if (this[field] === null || this[field] === '') {
|
||||
alert('Product Error', `Please fill in required fields: ${field}.`, 'warning');
|
||||
return; // Exit early if validation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed to send the data as raw JSON string
|
||||
const response = await fetch('/InvMainAPI/AddSupplier', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData) // Convert the formData to a JSON string
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
|
||||
} else {
|
||||
alert('Success!', 'Supplier form has been successfully submitted.', 'success');
|
||||
this.fetchSuppliers();
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Product Error', `An error occurred: ${error.message}`, 'error');
|
||||
}
|
||||
finally {
|
||||
await new Promise(resolve => {
|
||||
$('#loadingModal').on('shown.bs.modal', resolve);
|
||||
});
|
||||
$('#loadingModal').modal('hide');
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.supplierCompName = null;
|
||||
this.supplierAddress = null;
|
||||
this.supplierEmail = null;
|
||||
this.supplierPhoneNo = null;
|
||||
this.supplierPIC = null;
|
||||
},
|
||||
initiateTable() {
|
||||
self = this;
|
||||
this.supplierDatatable = $('#supplierDatatable').DataTable({
|
||||
"data": this.suppliers,
|
||||
"columns": [
|
||||
{
|
||||
"title": "Supplier Company Name",
|
||||
"data": "supplierCompName",
|
||||
},
|
||||
{
|
||||
"title": "Supplier Address",
|
||||
"data": "supplierAddress",
|
||||
},
|
||||
{
|
||||
"title": "Company PIC",
|
||||
"data": "supplierPIC",
|
||||
},
|
||||
{
|
||||
"title": "Supplier Email",
|
||||
"data": "supplierEmail",
|
||||
},
|
||||
{
|
||||
"title": "Supplier Phone No",
|
||||
"data": "supplierPhoneNo",
|
||||
},
|
||||
{
|
||||
"title": "Delete",
|
||||
"data": "supplierId",
|
||||
"render": function (data) {
|
||||
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
|
||||
return deleteButton;
|
||||
},
|
||||
}
|
||||
],
|
||||
responsive: true,
|
||||
|
||||
})
|
||||
|
||||
// Attach click event listener to the delete buttons
|
||||
$('#supplierDatatable tbody').on('click', '.delete-btn', function () {
|
||||
const supplierId = $(this).data('id');
|
||||
self.deleteSupplier(supplierId);
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
async deleteSupplier(supplierId) {
|
||||
if (!confirm("Are you sure you want to delete this supplier?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/DeleteSupplier/${supplierId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ supplierId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
this.errorMessage = 'Error: ' + (errorData.message || 'Unknown error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchSuppliers();
|
||||
} catch (error) {
|
||||
console.error('Error deleting supplier:', error);
|
||||
this.errorMessage = 'Error: ' + error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
867
Areas/Inventory/Views/ItemMovement/ItemMovementUser.cshtml
Normal file
867
Areas/Inventory/Views/ItemMovement/ItemMovementUser.cshtml
Normal file
@ -0,0 +1,867 @@
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Item Movement";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
<style>
|
||||
.text-true {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.text-false {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: blue; /* Warna asal untuk 'Receive' */
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: orange; /* Warna oren untuk 'Return' */
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: greenyellow;
|
||||
}
|
||||
|
||||
.ms-auto {
|
||||
margin-left: auto !important; /* Push Complete/Incomplete to right */
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
|
||||
|
||||
<div id="ItemMovement" class="row">
|
||||
<div class="row mb-3">
|
||||
<h2 for="sortSelect" class="col-sm-1 col-form-h2" style="min-width:150px;">Sort by:</h2>
|
||||
<div class="col-sm-4">
|
||||
<select id="sortSelect" class="form-control" v-model="sortBy" v-on:change="handleSorting">
|
||||
<option value="all">All</option>
|
||||
<option value="item">Item</option>
|
||||
<option value="station">Station</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" v-if="sortBy === 'item'">
|
||||
<h4 class="col-sm-1 col-form-h2" style="min-width:150px;">Search Item:</h4>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" v-model="searchQuery" placeholder="Search by item code...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" v-if="sortBy === 'station'">
|
||||
<h4 class="col-sm-1 col-form-h2" style="min-width:150px;">Search Station:</h4>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" v-model="searchStation" placeholder="Search by station name...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="sortBy === 'all'">
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h2>Pending Item Movement</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="itemMovementNotCompleteDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h2>Complete Item Movement</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="itemMovementCompleteDatatable" style="width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------ITEM CATEGORY---------------------------------------------------------------------->
|
||||
<div v-if="sortBy === 'item'">
|
||||
<div v-for="(group, itemId) in filteredItems" :key="itemId" class="row card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Item : {{ group.uniqueID }}</h2>
|
||||
<button class="btn btn-light" v-on:click="toggleCategory(itemId)">
|
||||
<i :class="categoryVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> Show Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hide all details unless button is clicked -->
|
||||
<div v-show="categoryVisible[itemId]" class="card-body">
|
||||
<div v-for="(movement, index) in group.movements.sort((a, b) => a.id - b.id).reverse()" :key="movement.id" class="movement-row">
|
||||
|
||||
<div v-if="index === 0" class="row">
|
||||
<strong>Latest Movement</strong>
|
||||
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
|
||||
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
|
||||
'text-success': movement.toStation !== null, 'text-info': movement.action === 'Assign' && movement.toStation === null}"
|
||||
class="flex-shrink-0 text-nowrap" style="max-width:90px; min-width:90px;">
|
||||
|
||||
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.toStation !== null ? 'Change' : 'Assign')) }}
|
||||
|
||||
</h3>
|
||||
|
||||
<!-- Send Date -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:285px; min-width:285px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : 'Send Date'}}</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.sendDate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Receive Date -->
|
||||
<div v-if="movement.action !== 'Assign'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:160px;">{{ movement.receiveDate || 'Not arrive' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.action }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
|
||||
</div>
|
||||
|
||||
<!-- More Details Button -->
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
|
||||
More Details
|
||||
</button>
|
||||
|
||||
<!-- Completion Status -->
|
||||
<h4 :class="movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : 'text-danger'"
|
||||
class="text-nowrap ms-3">
|
||||
{{ movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete') }}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
<!-- Conditionally render Start Icon -->
|
||||
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
|
||||
<i v-else class="fas fa-warehouse fa-2x"></i>
|
||||
<p><strong>Start</strong></p>
|
||||
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
|
||||
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
|
||||
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<p></p>
|
||||
<i class="fas fa-arrow-right fa-2x"></i>
|
||||
<p>{{ movement.latestStatus || movement.toOther }}</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)">
|
||||
Remark
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)">
|
||||
Consignment Note
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<!-- Conditionally render End Icon -->
|
||||
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
|
||||
<i v-else class="fas fa-user fa-2x"></i>
|
||||
<p><strong>End</strong></p>
|
||||
<p v-if="movement.lastUser !== null"><strong>User:</strong> {{ movement.lastUserName }}</p>
|
||||
<p v-if="movement.lastStation !== null"><strong>Station:</strong> {{ movement.lastStationName }}</p>
|
||||
<p v-if="movement.lastStore !== null"><strong>Store:</strong> {{ movement.lastStoreName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-light w-100 text-left" v-on:click="toggleHistory(itemId)">
|
||||
<i :class="historyVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> View History
|
||||
</button>
|
||||
|
||||
<div v-show="historyVisible[itemId]" class="history-row">
|
||||
<div v-for="(movement, i) in group.movements.slice(1)" :key="i" class="row mt-2">
|
||||
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
|
||||
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
|
||||
'text-success': movement.toStation !== null, 'text-info': movement.action === 'Assign' && movement.toStation === null}"
|
||||
class="flex-shrink-0 text-nowrap" style="max-width:90px; min-width:90px;">
|
||||
|
||||
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.toStation !== null ? 'Change' : 'Assign')) }}
|
||||
|
||||
</h3>
|
||||
|
||||
<!-- Send Date -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:285px; min-width:285px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : 'Send Date'}}</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.sendDate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Receive Date -->
|
||||
<div v-if="movement.action !== 'Assign'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:160px;">{{ movement.receiveDate || 'Not arrive' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.action }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
|
||||
</div>
|
||||
|
||||
<!-- More Details Button -->
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
|
||||
More Details
|
||||
</button>
|
||||
|
||||
<!-- Completion Status -->
|
||||
<h4 :class="movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : 'text-danger'"
|
||||
class="text-nowrap ms-3">
|
||||
{{ movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
<!-- Conditionally render Start Icon -->
|
||||
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
|
||||
<i v-else class="fas fa-warehouse fa-2x"></i>
|
||||
<p><strong>Start</strong></p>
|
||||
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
|
||||
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
|
||||
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<p></p>
|
||||
<i class="fas fa-arrow-right fa-2x"></i>
|
||||
<p>{{ movement.latestStatus || movement.toOther }}</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)">
|
||||
Remark
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)">
|
||||
Consignment Note
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<!-- Conditionally render End Icon -->
|
||||
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
|
||||
<i v-else class="fas fa-user fa-2x"></i>
|
||||
<p><strong>End</strong></p>
|
||||
<p v-if="movement.lastUser !== null"><strong>User:</strong> {{ movement.lastUserName }}</p>
|
||||
<p v-if="movement.lastStation !== null"><strong>Station:</strong> {{ movement.lastStationName }}</p>
|
||||
<p v-if="movement.lastStore !== null"><strong>Store:</strong> {{ movement.lastStoreName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--------------------------------------------STATION CATEGORY---------------------------------------------------------------------->
|
||||
<div v-if="sortBy === 'station'">
|
||||
<div v-for="(items, station) in filteredStation" :key="stationName" :class="{'bg-light-gray': station === 'Unassign Station', 'bg-white': station !== 'Unassign Station'}" class="station-category card mt-3">
|
||||
<!-- Station Header -->
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h3>{{ station }}</h3>
|
||||
<button class="btn btn-light" v-on:click="toggleCategory(station)">
|
||||
<i :class="categoryVisible[station] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> Show Items
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Show Items Under Each Station -->
|
||||
<div v-show="categoryVisible[station]" class="card-body">
|
||||
<div v-for="(group, itemId) in items" :key="itemId" class="row card">
|
||||
<!-- Item Header -->
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Item : {{ group.uniqueID }}</h2>
|
||||
<button class="btn btn-light" v-on:click="toggleCategory(itemId)">
|
||||
<i :class="categoryVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> Show Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Show Movements for Each Item -->
|
||||
<div v-show="categoryVisible[itemId]" class="card-body">
|
||||
<div v-for="(movement, index) in group.movements.sort((a, b) => a.id - b.id).reverse()" :key="movement.id" class="movement-row">
|
||||
<div v-if="index === 0" class="row">
|
||||
<strong>Latest Movement</strong>
|
||||
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
|
||||
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
|
||||
'text-success': movement.toStation !== null, 'text-info': movement.action === 'Assign' && movement.toStation === null}"
|
||||
class="flex-shrink-0 text-nowrap" style="max-width:90px; min-width:90px;">
|
||||
|
||||
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.toStation !== null ? 'Change' : 'Assign')) }}
|
||||
|
||||
</h3>
|
||||
|
||||
<!-- Send Date -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:285px; min-width:285px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : 'Send Date'}}</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.sendDate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Receive Date -->
|
||||
<div v-if="movement.action !== 'Assign'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:160px;">{{ movement.receiveDate || 'Not arrive' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.action }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
|
||||
</div>
|
||||
|
||||
<!-- More Details Button -->
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
|
||||
More Details
|
||||
</button>
|
||||
|
||||
<!-- Completion Status -->
|
||||
<h4 :class="movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : 'text-danger'"
|
||||
class="text-nowrap ms-3">
|
||||
{{ movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
|
||||
<i v-else class="fas fa-warehouse fa-2x"></i>
|
||||
<p><strong>Start</strong></p>
|
||||
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
|
||||
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
|
||||
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<i class="fas fa-arrow-right fa-2x"></i>
|
||||
<p>{{ movement.latestStatus || movement.toOther }}</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)">
|
||||
Remark
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)">
|
||||
Consignment Note
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
|
||||
<i v-else class="fas fa-user fa-2x"></i>
|
||||
<p><strong>End</strong></p>
|
||||
<p v-if="movement.lastUser !== null"><strong>User:</strong> {{ movement.lastUserName }}</p>
|
||||
<p v-if="movement.lastStation !== null"><strong>Station:</strong> {{ movement.lastStationName }}</p>
|
||||
<p v-if="movement.lastStore !== null"><strong>Store:</strong> {{ movement.lastStoreName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single View History Button -->
|
||||
<button class="btn btn-light w-100 text-left" v-on:click="toggleHistory(itemId)">
|
||||
<i :class="historyVisible[itemId] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i> View History
|
||||
</button>
|
||||
|
||||
<div v-show="historyVisible[itemId]" class="history-row">
|
||||
<div v-for="(movement, i) in group.movements.slice(1)" :key="i" class="row mt-2">
|
||||
<div class="col-md-12 d-flex flex-wrap align-items-center gap-3 p-2 border-bottom">
|
||||
<!-- Movement Type -->
|
||||
<h3 :class="{'text-primary': movement.toOther === 'On Delivery', 'text-warning': movement.toOther === 'Return',
|
||||
'text-success': movement.toStation !== null, 'text-info': movement.action === 'Assign' && movement.toStation === null}"
|
||||
class="flex-shrink-0 text-nowrap" style="max-width:90px; min-width:90px;">
|
||||
|
||||
{{ movement.toOther === 'Return' ? 'Return' : (movement.toOther === 'On Delivery' ? 'Receive' : ( movement.toStation !== null ? 'Change' : 'Assign')) }}
|
||||
|
||||
</h3>
|
||||
|
||||
<!-- Send Date -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:285px; min-width:285px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">{{movement.action === 'Assign' ? 'Assign Date' : 'Send Date'}}</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.sendDate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Receive Date -->
|
||||
<div v-if="movement.action !== 'Assign'" class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:290px; min-width:290px;">
|
||||
<h4 class="fixed-label m-0 text-nowrap">Receive Date:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:160px;">{{ movement.receiveDate || 'Not arrive' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:150px; min-width:150px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Action:</h4>
|
||||
<span class="fixed-value text-truncate">{{ movement.action }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 flex-grow-1" style="max-width:160px; min-width:160px;">
|
||||
<h4 class="fixed-labelStatus m-0 text-nowrap">Status:</h4>
|
||||
<span class="fixed-value text-truncate" style="max-width:90px;">{{ movement.latestStatus || movement.toOther }}</span>
|
||||
</div>
|
||||
|
||||
<!-- More Details Button -->
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="toggleDetails(movement.id)">
|
||||
More Details
|
||||
</button>
|
||||
|
||||
<!-- Completion Status -->
|
||||
<h4 :class="movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'text-success' : 'text-danger'"
|
||||
class="text-nowrap ms-3">
|
||||
{{ movement.movementComplete == 1 && movement.latestStatus !== 'Ready To Deploy' ? 'Complete' : (movement.latestStatus === 'Ready To Deploy' ? 'Canceled' : 'Incomplete') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Details Section (Hidden by Default) -->
|
||||
<div v-show="detailsVisible[movement.id]" class="col-md-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
<!-- Conditionally render Start Icon -->
|
||||
<i v-if="movement.toStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-user fa-2x"></i>
|
||||
<i v-else class="fas fa-warehouse fa-2x"></i>
|
||||
<p><strong>Start</strong></p>
|
||||
<p v-if="movement.toUser !== null"><strong>User:</strong> {{ movement.toUserName }}</p>
|
||||
<p v-if="movement.toStation !== null"><strong>Station:</strong> {{ movement.toStationName }}</p>
|
||||
<p v-if="movement.toStore !== null"><strong>Store:</strong> {{ movement.toStoreName }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<p></p>
|
||||
<i class="fas fa-arrow-right fa-2x"></i>
|
||||
<p>{{ movement.latestStatus || movement.toOther }}</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="remark(movement.remark)">
|
||||
Remark
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-info btn-sm ms-auto" v-on:click="consignmentNote(movement.consignmentNote)">
|
||||
Consignment Note
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<!-- Conditionally render End Icon -->
|
||||
<i v-if="movement.lastStation" class="fas fa-map-marker-alt"></i>
|
||||
<i v-else-if="movement.toOther !== 'On Delivery'" class="fas fa-warehouse fa-2x"></i>
|
||||
<i v-else class="fas fa-user fa-2x"></i>
|
||||
<p><strong>End</strong></p>
|
||||
<p v-if="movement.lastUser !== null"><strong>User:</strong> {{ movement.lastUserName }}</p>
|
||||
<p v-if="movement.lastStation !== null"><strong>Station:</strong> {{ movement.lastStationName }}</p>
|
||||
<p v-if="movement.lastStore !== null"><strong>Store:</strong> {{ movement.lastStoreName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------REMARK & CONSIGNMENT NOTE CATEGORY---------------------------------------------------------------------->
|
||||
<div class="modal fade" id="remarkModal" tabindex="-1" aria-labelledby="remarkModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="remarkModalLabel">Remark</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="remarkContent">
|
||||
<!-- Remark Content Here -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="consignmentModal" tabindex="-1" aria-labelledby="consignmentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="consignmentModalLabel">Consignment Note</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img v-if="/\.(jpeg|jpg|png|gif)$/i.test(consignmentNoteUrl)" :src="consignmentNoteUrl" class="img-fluid" alt="Consignment Note Image">
|
||||
<iframe v-else-if="/\.pdf$/i.test(consignmentNoteUrl)" :src="consignmentNoteUrl" style="width:100%; height: 80vh;"></iframe>
|
||||
<a v-else class="btn btn-primary">There's no Folder or Picture</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#ItemMovement');
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
itemMovements: [],
|
||||
itemMovementCompleteDatatable: null,
|
||||
stationDatatable: null,
|
||||
itemMovementNotCompleteDatatable: null,
|
||||
searchQuery: "",
|
||||
searchStation: "",
|
||||
sortBy: "all",
|
||||
historyVisible: {},
|
||||
detailsVisible: {},
|
||||
categoryVisible: {},
|
||||
consignmentNoteUrl: "",
|
||||
stationName: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
processedGroupedItems() {
|
||||
let grouped = this.itemMovements.reduce((acc, movement) => {
|
||||
if (!acc[movement.itemId]) {
|
||||
acc[movement.itemId] = {
|
||||
uniqueID: movement.uniqueID,
|
||||
movements: [],
|
||||
};
|
||||
}
|
||||
acc[movement.itemId].movements.push(movement);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Sort items from newest to oldest & filter them
|
||||
for (let itemId in grouped) {
|
||||
let movements = grouped[itemId].movements
|
||||
.sort((a, b) => b.id - a.id); // Newest to oldest
|
||||
|
||||
let stopIndex = movements.findIndex(m =>
|
||||
m.toOther === 'Return' && m.movementComplete == 1
|
||||
);
|
||||
|
||||
if (stopIndex !== -1) {
|
||||
movements = movements.slice(0, stopIndex);
|
||||
}
|
||||
|
||||
grouped[itemId].movements = movements;
|
||||
}
|
||||
|
||||
return grouped;
|
||||
},
|
||||
|
||||
groupedByStation() {
|
||||
let groupedByItem = this.itemMovements.reduce((acc, movement) => {
|
||||
if (!acc[movement.uniqueID]) {
|
||||
acc[movement.uniqueID] = {
|
||||
uniqueID: movement.uniqueID,
|
||||
movements: [],
|
||||
};
|
||||
}
|
||||
acc[movement.uniqueID].movements.push(movement);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let groupedByStation = {};
|
||||
|
||||
Object.keys(groupedByItem).forEach(itemId => {
|
||||
let movements = groupedByItem[itemId].movements
|
||||
.sort((a, b) => b.id - a.id); // Newest → Oldest
|
||||
|
||||
// Find first occurrence of 'Return' complete
|
||||
let stopIndex = movements.findIndex(m =>
|
||||
m.toOther === 'Return' && m.movementComplete == 1
|
||||
);
|
||||
|
||||
// Remove older movements
|
||||
if (stopIndex !== -1) {
|
||||
movements = movements.slice(0, stopIndex);
|
||||
}
|
||||
|
||||
if (movements.length > 0) {
|
||||
let latestMovement = movements[0];
|
||||
let station = latestMovement.lastStationName || latestMovement.toStationName || "Self Assigned";
|
||||
|
||||
if (!groupedByStation[station]) {
|
||||
groupedByStation[station] = {};
|
||||
}
|
||||
|
||||
groupedByStation[station][itemId] = { uniqueID: itemId, movements };
|
||||
}
|
||||
});
|
||||
|
||||
// 4️⃣ **Sort stations & move 'Unassign Station' to last**
|
||||
let sortedKeys = Object.keys(groupedByStation).sort((a, b) => {
|
||||
if (a === "Unassign Station") return 1;
|
||||
if (b === "Unassign Station") return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
let sortedGrouped = {};
|
||||
sortedKeys.forEach(key => {
|
||||
sortedGrouped[key] = groupedByStation[key];
|
||||
});
|
||||
|
||||
return sortedGrouped;
|
||||
},
|
||||
|
||||
filteredItems() {
|
||||
if (!this.searchQuery.trim()) {
|
||||
return this.processedGroupedItems;
|
||||
}
|
||||
const searchLower = this.searchQuery.toLowerCase();
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.processedGroupedItems).filter(([_, group]) =>
|
||||
group.uniqueID.toLowerCase().includes(searchLower)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
filteredStation() {
|
||||
if (!this.searchStation) {
|
||||
return this.groupedByStation;
|
||||
}
|
||||
|
||||
let searchQuery = this.searchStation.toLowerCase();
|
||||
let grouped = this.groupedByStation;
|
||||
let filtered = {};
|
||||
|
||||
Object.keys(grouped).forEach(station => {
|
||||
if (station.toLowerCase().includes(searchQuery)) {
|
||||
filtered[station] = grouped[station];
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchItemMovement();
|
||||
},
|
||||
methods: {
|
||||
remark(remark) {
|
||||
document.getElementById("remarkContent").innerText = remark || "No remark message provide.";
|
||||
let modal = new bootstrap.Modal(document.getElementById("remarkModal"));
|
||||
modal.show();
|
||||
},
|
||||
|
||||
consignmentNote(consignmentNote) {
|
||||
if (!consignmentNote) {
|
||||
this.consignmentNoteUrl = "No consignment note available.";
|
||||
new bootstrap.Modal(document.getElementById('consignmentModal')).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pastikan URL betul
|
||||
this.consignmentNoteUrl = consignmentNote;
|
||||
|
||||
// Tunggu Vue update sebelum buka modal
|
||||
this.$nextTick(() => {
|
||||
new bootstrap.Modal(document.getElementById('consignmentModal')).show();
|
||||
});
|
||||
},
|
||||
|
||||
async fetchItemMovement() {
|
||||
try {
|
||||
const response = await fetch("/InvMainAPI/ItemMovementUser", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to fetch item movement");
|
||||
|
||||
const data = await response.json();
|
||||
this.itemMovements = data.map((movement) => ({
|
||||
...movement,
|
||||
showDetails: false,
|
||||
}));
|
||||
|
||||
this.renderTables();
|
||||
} catch (error) {
|
||||
console.error("Error fetching item:", error);
|
||||
}
|
||||
},
|
||||
|
||||
renderTables() {
|
||||
if (this.sortBy === "all") {
|
||||
this.initAllTables();
|
||||
}
|
||||
},
|
||||
|
||||
initAllTables() {
|
||||
if (this.itemMovementNotCompleteDatatable) {
|
||||
this.itemMovementNotCompleteDatatable.destroy();
|
||||
}
|
||||
if (this.itemMovementCompleteDatatable) {
|
||||
this.itemMovementCompleteDatatable.destroy();
|
||||
}
|
||||
if (this.stationDatatable) {
|
||||
this.stationDatatable.destroy();
|
||||
}
|
||||
|
||||
// Get latest movement per uniqueID
|
||||
function getLatestMovements(data) {
|
||||
let latestMovements = {};
|
||||
data.forEach(movement => {
|
||||
let id = movement.uniqueID;
|
||||
if (!latestMovements[id] || latestMovements[id].id < movement.id) {
|
||||
latestMovements[id] = movement;
|
||||
}
|
||||
});
|
||||
return Object.values(latestMovements);
|
||||
}
|
||||
|
||||
// Distribute items based on priority
|
||||
let latestMovements = getLatestMovements(this.itemMovements);
|
||||
let notCompleteData = [];
|
||||
let completeData = [];
|
||||
let assignedData = [];
|
||||
|
||||
latestMovements.forEach(movement => {
|
||||
if (movement.movementComplete == 0) {
|
||||
notCompleteData.push(movement);
|
||||
} else if (movement.movementComplete == 1) {
|
||||
completeData.push(movement);
|
||||
}
|
||||
});
|
||||
|
||||
// Table 1: Not Complete Movements
|
||||
this.itemMovementNotCompleteDatatable = $("#itemMovementNotCompleteDatatable").DataTable({
|
||||
data: notCompleteData,
|
||||
columns: [
|
||||
{ title: "Unique Id", data: "id" },
|
||||
{ title: "Product Name", data: "productName" },
|
||||
{ title: "Product Code", data: "uniqueID" },
|
||||
{ title: "Action", data: "action" },
|
||||
{ title: "Send Date", data: "sendDate" },
|
||||
{ title: "Start Status", data: "toOther" },
|
||||
{ title: "From User", data: "toUserName" },
|
||||
{ title: "Last User", data: "lastUserName" },
|
||||
{ title: "From Station", data: "toStationName" },
|
||||
{ title: "From Store", data: "toStoreName" },
|
||||
{ title: "Quantity", data: "quantity" },
|
||||
{ title: "Note", data: "consignmentNote", render: renderFile },
|
||||
{ title: "Remark", data: "remark" },
|
||||
],
|
||||
responsive: true,
|
||||
});
|
||||
|
||||
// Table 2: Completed Movements
|
||||
this.itemMovementCompleteDatatable = $("#itemMovementCompleteDatatable").DataTable({
|
||||
data: completeData,
|
||||
columns: [
|
||||
{ title: "Unique Id", data: "id" },
|
||||
{ title: "Product Name", data: "productName" },
|
||||
{ title: "Product Code", data: "uniqueID" },
|
||||
{ title: "Send Date", data: "sendDate" },
|
||||
{ title: "Receive Date", data: "receiveDate" },
|
||||
{ title: "Action", data: "action" },
|
||||
{ title: "Start Status", data: "toOther" },
|
||||
{ title: "Latest Status", data: "latestStatus" },
|
||||
{ title: "From User", data: "toUserName" },
|
||||
{ title: "Last User", data: "lastUserName" },
|
||||
{ title: "From Station", data: "toStationName" },
|
||||
{ title: "Last Station", data: "lastStationName" },
|
||||
{ title: "From Store", data: "toStoreName" },
|
||||
{ title: "Last Store", data: "lastStoreName" },
|
||||
{ title: "Qty", data: "quantity" },
|
||||
{ title: "Note", data: "consignmentNote", render: renderFile },
|
||||
{ title: "Remark", data: "remark" },
|
||||
],
|
||||
responsive: true,
|
||||
});
|
||||
|
||||
// Function to render file (image/PDF)
|
||||
function renderFile(data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
|
||||
var isPdf = /\.pdf$/i.test(data);
|
||||
if (isImage) {
|
||||
return `<a href="${data}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
} else if (isPdf) {
|
||||
return `<a href="${data}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
} else {
|
||||
return `<a href="${data}" target="_blank">Download File</a>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
toggleCategory(itemId) {
|
||||
this.categoryVisible[itemId] = !this.categoryVisible[itemId];
|
||||
|
||||
this.detailsVisible = {};
|
||||
this.historyVisible = {};
|
||||
},
|
||||
|
||||
toggleHistory(itemId) {
|
||||
// Jika item yang ditekan sudah terbuka, tutup
|
||||
if (this.historyVisible[itemId]) {
|
||||
this.historyVisible[itemId] = false;
|
||||
} else {
|
||||
// Tutup semua history lain dahulu
|
||||
this.historyVisible = {};
|
||||
|
||||
// Buka hanya item yang ditekan
|
||||
this.historyVisible[itemId] = true;
|
||||
}
|
||||
},
|
||||
|
||||
toggleDetails(movementId) {
|
||||
this.detailsVisible[movementId] = !this.detailsVisible[movementId];
|
||||
},
|
||||
|
||||
handleSorting() {
|
||||
this.renderTables();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
}
|
||||
843
Areas/Inventory/Views/ItemMovement/ItemRequest.cshtml
Normal file
843
Areas/Inventory/Views/ItemMovement/ItemRequest.cshtml
Normal file
@ -0,0 +1,843 @@
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Product Request";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
|
||||
<style>
|
||||
.dropdown {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-toggle-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-toggle-box input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
border: none;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dropdown-content option {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content option:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.modal-body img {
|
||||
max-width: 100%; /* Ensure it fits the modal */
|
||||
max-height: 150px; /* Adjust max height */
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="requestProduct" class="row">
|
||||
|
||||
<!-- Document Preview Modal -->
|
||||
<div id="documentModal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Document Preview</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="documentPreview" src="" alt="Document" class="img-fluid" style="max-height: 80vh;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h2>Pending Request</h2>
|
||||
<button id="addRequestBtn" class="btn btn-success col-md-3 col-lg-3 m-1 col-12"><i class="fa fa-plus"></i> Add Request</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* <div v-if="loading">
|
||||
<div class="spinner-border text-info" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div> *@
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="requestDatatable" style=" width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<h2>Complete Request</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@* <div v-if="loading">
|
||||
<div class="spinner-border text-info" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div> *@
|
||||
<table class="table table-bordered table-hover table-striped no-wrap" id="settledrequestDatatable" style=" width:100%;border-style: solid; border-width: 1px"></table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal fade" id="requestModal" tabindex="-1" role="dialog" aria-labelledby="addRequestModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addRequestModalLabel">Add Request</h5>
|
||||
<button type="button" class="closeModal" data-dismiss="modal" aria-label="Close" v-on:click="showRequestModal=false">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<form v-on:submit.prevent="addRequest" data-aos="fade-right">
|
||||
<div class=" register" data-aos="fade-right">
|
||||
<div class="row" data-aos="fade-right">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<h3 class="register-heading">PRODUCT REQUEST</h3>
|
||||
<div class="row register-form">
|
||||
<div class="col-md-13">
|
||||
|
||||
<!-- Product Name -->
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Product</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown" v-click-outside="closeDropdown">
|
||||
<!-- Button + Input dalam satu box -->
|
||||
<div class="dropdown-toggle-box" v-on:click="dropdownOpen = !dropdownOpen">
|
||||
<input type="text" class="form-control" v-model="searchQuery"
|
||||
placeholder="Search product..." v-on:focus="dropdownOpen = true" v-on:click.stop />
|
||||
<button type="button" class="btn btn-primary dropdown-btn" v-on:click.stop="dropdownOpen = !dropdownOpen">
|
||||
▼
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown list -->
|
||||
<div v-if="dropdownOpen" class="dropdown-content" v-on:click.stop>
|
||||
<div v-for="(item, index) in filteredProducts"
|
||||
:key="index" class="dropdown-item" v-on:mousedown.prevent="selectProduct(item)">
|
||||
{{ item.productName + ' (' + item.modelNo + ')' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Dropdown -->
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Category:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" id="category" name="category" v-model="showProduct.category" class="form-control" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Product Dropdown-->
|
||||
<div class="form-group row align-items-center">
|
||||
<label for="imageProduct" class="col-sm-2 col-form-label">Product Image: </label>
|
||||
<div class="col-sm-8">
|
||||
<img v-if="showProduct.imageProduct" :src="showProduct.imageProduct" alt="Product Image" class="img-fluid" data-toggle="modal" data-target="#imageModal" />
|
||||
<input type="hidden" id="imageProduct" name="imageProduct" v-model="showProduct">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="imageModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="imageModalLabel">Product Image</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img :src="showProduct.imageProduct" alt="Product Image" class="img-fluid">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quantity Input -->
|
||||
<div class="form-group row d-flex align-items-center">
|
||||
<label class="col-sm-2 col-form-label">Quantity:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" class="form-control" v-model="quantity" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Who will assign to *@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Item Assignment : </label>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<select class="btn btn-primary dropdown-toggle col-md-12" v-model="assign"required>
|
||||
<option class="btn-light" value="" selected disabled>Select Assignment Type</option>
|
||||
<option class="btn-light" v-for="(item, index) in assigns" :key="item" :value="item">{{ item ?? 'Select Assignment Type' }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Station Dropdown -->
|
||||
<div class="form-group row" v-if="assign === 'Station'">
|
||||
<label class="col-sm-2 col-form-label">Station:</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="dropdown">
|
||||
<select class="btn btn-primary dropdown-toggle col-md-12" data-toggle="dropdown" aria-expanded="false" v-model="stationId" required>
|
||||
<option class="btn-light" value="" disabled selected>Select Station</option>
|
||||
<option v-if="stations.length === 0" class="btn-light" disabled>No Station Assigned to You</option>
|
||||
<option class="btn-light" v-for="(item, index) in stations" :key="index" :value="item.stationId">
|
||||
{{ item.stationName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remark Input (Wider) -->
|
||||
<div class="form-group row d-flex align-items-center">
|
||||
<label class="col-sm-2 col-form-label">Remark:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control col-md-10" v-model="remark" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document/Picture Input -->
|
||||
<div class="form-group row d-flex align-items-center">
|
||||
<label class="col-sm-2 col-form-label">Document/Picture:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="file" id="document" name="document" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@* Submit and Reset Buttons *@
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-8 offset-sm-3">
|
||||
<button type="button" v-on:click="resetForm" class="btn btn-secondary m-1">Reset</button>
|
||||
<button type="submit" class="btn btn-primary m-1">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#requestProduct');
|
||||
|
||||
// Attach a click event listener to elements with the class 'btn-success'.
|
||||
$('#addRequestBtn').on('click', function () {
|
||||
$('#requestModal').modal('show');
|
||||
});
|
||||
$('.closeModal').on('click', function () {
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
requestID : null,
|
||||
userId : null,
|
||||
stationId : "",
|
||||
productId : "",
|
||||
remark: "",
|
||||
document: null,
|
||||
quantity: 0,
|
||||
status: "",
|
||||
requestDate : null,
|
||||
approvalDate : null,
|
||||
productCategory: "",
|
||||
assign: "",
|
||||
|
||||
productName: null,
|
||||
searchQuery: "",
|
||||
dropdownOpen: false,
|
||||
stations: [],
|
||||
selectedProduct: "",
|
||||
selectedStation: "",
|
||||
selectedCategory: "",
|
||||
assigns: ["Self-Assign", "Station"],
|
||||
showRequestModal: false,
|
||||
loading: false,
|
||||
products: [],
|
||||
request: [],
|
||||
currentUser: null,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.fetchProducts();
|
||||
await this.fetchUser();
|
||||
await Promise.all([
|
||||
this.fetchStation(),
|
||||
this.fetchRequest(),
|
||||
]);
|
||||
},
|
||||
computed: {
|
||||
showProduct() {
|
||||
if (!this.productId) {
|
||||
return []; // No company selected, return empty list
|
||||
}
|
||||
const product = this.products.find(c => c.productId === this.productId);
|
||||
|
||||
this.productCategory = product.category;
|
||||
|
||||
return product ? product : {};
|
||||
},
|
||||
filteredProducts() {
|
||||
return this.products.filter(item =>
|
||||
item.productName.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
||||
item.modelNo.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDropdown() {
|
||||
this.dropdownOpen = false; // Tutup dropdown
|
||||
},
|
||||
|
||||
selectProduct(item) {
|
||||
this.selectedProduct = item;
|
||||
this.productId = item.productId;
|
||||
this.searchQuery = item.productName + " (" + item.modelNo + ")";
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.document = e.target.result.split(',')[1]; // Get Base64 string (remove metadata)
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
this.document = null;
|
||||
}
|
||||
},
|
||||
|
||||
async addRequest() {
|
||||
try {
|
||||
const requiredFields = ['productId', 'quantity', 'assign'];
|
||||
|
||||
// Loop through required fields and check if any are null or empty
|
||||
for (let field of requiredFields) {
|
||||
if (!this[field]) {
|
||||
alert('Request Error', `Please fill in required fields: ${field}.`, 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.assign === "Station"){
|
||||
if (this.stationId == null) {
|
||||
alert('Request Error', `Please fill in required fields : Station.`, 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.assign === "Self-Assign"){
|
||||
this.stationId = null;
|
||||
}
|
||||
|
||||
this.userId = this.currentUser.id;
|
||||
this.status = "Requested";
|
||||
|
||||
const now = new Date();
|
||||
this.requestDate = new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
|
||||
// Prepare data as JSON (No file upload)
|
||||
const requestData = {
|
||||
ProductId: this.productId,
|
||||
StationId: this.stationId,
|
||||
UserId: this.userId,
|
||||
ProductCategory: this.productCategory,
|
||||
RequestQuantity: this.quantity,
|
||||
remarkUser: this.remark || '',
|
||||
remarkMasterInv: '',
|
||||
status: this.status,
|
||||
requestDate: this.requestDate,
|
||||
approvalDate: null,
|
||||
Document: this.document
|
||||
};
|
||||
|
||||
$('.modal').modal('hide');
|
||||
|
||||
// Send the data to the API
|
||||
const response = await fetch('/InvMainAPI/AddRequest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.resetForm();
|
||||
alert('Success!', 'Request form has been successfully submitted.', 'success');
|
||||
const requestItem = await response.json();
|
||||
this.request.push(requestItem);
|
||||
this.fetchRequest();
|
||||
} else {
|
||||
throw new Error('Failed to submit form.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Inventory PSTW Error', `An error occurred: ${error.message}`, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
initiateTable() {
|
||||
self = this;
|
||||
this.requestDatatable = $('#requestDatatable').DataTable({
|
||||
"data": this.request.filter(request => request.status == "Requested"),
|
||||
"columns": [
|
||||
{
|
||||
"title": "Request ID",
|
||||
"data": "requestID",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
// Assign a unique ID to the <td> element
|
||||
$(td).attr('id', `qr${cellData}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Product Name",
|
||||
"data": "productName",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
var imageSrc = full.productPicture;
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(imageSrc);
|
||||
var isPdf = /\.pdf$/i.test(imageSrc);
|
||||
// var imageSrc = full.productImage; Fallback to data if imgsrc is unavailable
|
||||
console.log(full);
|
||||
|
||||
if (isImage) {
|
||||
return ` <div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${imageSrc}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Product Category",
|
||||
"data": "productCategory",
|
||||
},
|
||||
{
|
||||
"title": "Request Quantity",
|
||||
"data": "requestQuantity",
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Document / Picture",
|
||||
"data": "document",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
|
||||
var isPdf = /\.pdf$/i.test(data);
|
||||
|
||||
if (isImage) {
|
||||
return `<a href="${data}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<a href="${data}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
} else {
|
||||
return `<a href="${data}" target="_blank">Download File</a>`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Remark",
|
||||
"data": "remarkUser",
|
||||
},
|
||||
{
|
||||
"title": "Station Deploy",
|
||||
"data": "stationName",
|
||||
"render": function (data, type, full, meta) {
|
||||
return data ? data : "Self Assign";
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Request Date",
|
||||
"data": "requestDate",
|
||||
},
|
||||
{
|
||||
"title": "Status",
|
||||
"data": "status",
|
||||
},
|
||||
{
|
||||
"title": "Delete",
|
||||
"data": "requestID",
|
||||
"render": function (data) {
|
||||
var deleteButton = `<button type="button" class="btn btn-danger delete-btn" data-id="${data}">Delete</button>`;
|
||||
return deleteButton;
|
||||
},
|
||||
"className": "align-middle",
|
||||
}
|
||||
|
||||
],
|
||||
responsive: true,
|
||||
});
|
||||
this.requestDatatable = $('#settledrequestDatatable').DataTable({
|
||||
"data": this.request.filter(request => request.status !== "Requested"),
|
||||
"columns": [
|
||||
{
|
||||
"title": "Request ID",
|
||||
"data": "requestID",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
// Assign a unique ID to the <td> element
|
||||
$(td).attr('id', `qr${cellData}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Status",
|
||||
"data": "status",
|
||||
},
|
||||
{
|
||||
"title": "Product Name",
|
||||
"data": "productName",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
var imageSrc = full.productPicture;
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(imageSrc);
|
||||
var isPdf = /\.pdf$/i.test(imageSrc);
|
||||
// var imageSrc = full.productImage; Fallback to data if imgsrc is unavailable
|
||||
console.log(full);
|
||||
|
||||
if (isImage) {
|
||||
return ` <div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${imageSrc}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
}
|
||||
else if (isPdf) {
|
||||
return `<div class="row"><td>${data}</td></div>
|
||||
<a href="${imageSrc}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Product Category",
|
||||
"data": "productCategory",
|
||||
},
|
||||
{
|
||||
"title": "Request Quantity",
|
||||
"data": "requestQuantity",
|
||||
},
|
||||
{
|
||||
"title": "Station Deploy",
|
||||
"data": "stationName",
|
||||
"render": function (data, type, full, meta) {
|
||||
return data ? data : "Self Assign";
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Document / Picture",
|
||||
"data": "document",
|
||||
"render": function (data, type, full, meta) {
|
||||
if (!data) {
|
||||
return "No Document";
|
||||
}
|
||||
|
||||
// Check if the document is an image based on file extension
|
||||
var isImage = /\.(jpeg|jpg|png|gif)$/i.test(data);
|
||||
var isPdf = /\.pdf$/i.test(data);
|
||||
|
||||
if (isImage) {
|
||||
return `<a href="${data}" target="_blank" data-lightbox="image-1">
|
||||
<img src="${data}" alt="Image" class="img-thumbnail" style="width: 100px; height: 100px;" />
|
||||
</a>`;
|
||||
} else if (isPdf) {
|
||||
return `<a href="${data}" target="_blank">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/87/PDF_file_icon.svg"
|
||||
alt="PDF Document" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px;" />
|
||||
<br>View PDF
|
||||
</a>`;
|
||||
} else {
|
||||
return `<a href="${data}" target="_blank">Download File</a>`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Remark",
|
||||
"data": "remarkUser",
|
||||
},
|
||||
{
|
||||
"title": "Remark (Master)",
|
||||
"data": "remarkMasterInv",
|
||||
},
|
||||
{
|
||||
"title": "Request Date",
|
||||
"data": "requestDate",
|
||||
},
|
||||
{
|
||||
"title": "Approval Date",
|
||||
"data": "approvalDate",
|
||||
}
|
||||
|
||||
],
|
||||
responsive: true,
|
||||
});
|
||||
|
||||
$('#requestDatatable tbody').off('click', '.delete-btn');
|
||||
|
||||
$('#requestDatatable tbody').on('click', '.delete-btn', function () {
|
||||
const requestID = $(this).data('id');
|
||||
self.deleteRequestItem(requestID);
|
||||
});
|
||||
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async fetchRequest() {
|
||||
try
|
||||
{
|
||||
const response = await fetch(`/InvMainAPI/ItemRequestListEachUser/${this.userId}`,
|
||||
{
|
||||
method: 'GET', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Set content type
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch request List');
|
||||
}
|
||||
this.request = await response.json();
|
||||
|
||||
if ($.fn.dataTable.isDataTable('#requestDatatable')) {
|
||||
$('#requestDatatable').DataTable().clear().destroy();
|
||||
}
|
||||
if ($.fn.dataTable.isDataTable('#settledrequestDatatable')) {
|
||||
$('#settledrequestDatatable').DataTable().clear().destroy();
|
||||
}
|
||||
|
||||
this.initiateTable();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching request List:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchProducts() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/ProductList', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Set content type
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch products');
|
||||
}
|
||||
|
||||
this.products = await response.json();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching products:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchStation() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/StationList', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch suppliers');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.stations = data.filter(station => station.stationPicID === this.userId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUser() {
|
||||
try {
|
||||
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentUser = data?.userInfo || null;
|
||||
this.userId = await this.currentUser.id;
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.searchQuery = "";
|
||||
this.stationId = "";
|
||||
this.productId = "";
|
||||
this.remark = "";
|
||||
this.document = null;
|
||||
this.quantity = 0;
|
||||
this.status = "";
|
||||
this.requestDate = null;
|
||||
this.approvalDate = null;
|
||||
this.productCategory = "";
|
||||
this.assign = "",
|
||||
|
||||
this.productName = null;
|
||||
this.selectedProduct = "";
|
||||
this.selectedStation = "";
|
||||
this.selectedCategory = "";
|
||||
this.showRequestModal = false;
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async deleteRequestItem(requestID) {
|
||||
if (!confirm("Are you sure you want to delete this request?")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/DeleteRequest/${requestID}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
|
||||
if ($.fn.dataTable.isDataTable('#requestDatatable')) {
|
||||
const table = $('#requestDatatable').DataTable();
|
||||
table.row($(`.delete-btn[data-id="${requestID}"]`).closest('tr')).remove().draw();
|
||||
}
|
||||
|
||||
this.request = this.request.filter(req => req.requestID !== requestID);
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error deleting item:", error);
|
||||
alert("An error occurred while deleting the item.");
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
directives: {
|
||||
clickOutside: {
|
||||
beforeMount(el, binding) {
|
||||
el.clickOutsideEvent = (event) => {
|
||||
if (!(el.contains(event.target))) {
|
||||
binding.value?.(); // Guna optional chaining untuk elak error
|
||||
}
|
||||
};
|
||||
document.body.addEventListener("click", el.clickOutsideEvent);
|
||||
},
|
||||
unmounted(el) {
|
||||
document.body.removeEventListener("click", el.clickOutsideEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
}
|
||||
845
Areas/Inventory/Views/ItemMovement/QrUser.cshtml
Normal file
845
Areas/Inventory/Views/ItemMovement/QrUser.cshtml
Normal file
@ -0,0 +1,845 @@
|
||||
@{
|
||||
ViewData["Title"] = "QR Scanner";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
<style scoped>
|
||||
.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.barcode-format-checkbox {
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 200px; /* Adjust width as needed */
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
|
||||
<div id="registerItem" class="row">
|
||||
<div class="row card">
|
||||
<div class="card-header">
|
||||
<button v-if="displayStatus !== null" v-on:click="resetScanner" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Scanner
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<select v-if="displayStatus == null" class="form-select" v-model="selectedCameraId" v-on:change="updateCamera">
|
||||
<option v-for="device in videoInputDevices" :key="device.deviceId" :value="device.deviceId">
|
||||
{{ device.label || `Camera ${videoInputDevices.indexOf(device) + 1}` }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div id="registerItem" v-if="displayStatus == null" data-aos="fade-right">
|
||||
<p style="text-align:center; padding:10px;">Scan QR Code Here:</p>
|
||||
|
||||
<qrcode-stream :constraints="selectedConstraints"
|
||||
:formats="['qr_code']"
|
||||
:track="trackFunctionSelected.value"
|
||||
v-on:camera-on="onCameraReady"
|
||||
v-on:detect="onDecode"
|
||||
v-on:error="onError">
|
||||
</qrcode-stream>
|
||||
|
||||
<p class="error">{{ error }}</p>
|
||||
|
||||
</div>
|
||||
<!--RECEIVE OR RETURN INTERFACE -->
|
||||
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'arrived' && thisItem.currentStationId != null">
|
||||
<h2>Item Receive Information :</h2>
|
||||
<h3>Station Assign</h3>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'arrived' && thisItem.currentStationId == null">
|
||||
<h2>Item Receive Information :</h2>
|
||||
<h3>Self Assign</h3>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'return' && thisItem.currentStationId != null">
|
||||
<h2>Item Return Information :</h2>
|
||||
<h3>Station</h3>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 20px 0;" v-if="displayStatus === 'return' && thisItem.currentStationId == null">
|
||||
<h2>Item Return Information :</h2>
|
||||
<h3>Self Assign</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="displayStatus === 'arrived' || displayStatus === 'return'" style="display: flex; justify-content: center; align-items: center;">
|
||||
|
||||
<div class="col-lg-7 col-11 border rounded p-3 shadow-sm">
|
||||
<div class="row m-3 d-flex align-items-center justify-content-center">
|
||||
<div class="col-lg-7 col-11 border rounded p-3 shadow-sm">
|
||||
<div class="col-12 text-center">
|
||||
<img :src="thisItem.imageProduct" alt="Product Image" class="img-fluid rounded" data-toggle="modal" data-target="#imageModal" style="max-height: 300px;" />
|
||||
</div>
|
||||
<div class="col-12 text-center mt-3">
|
||||
<p class="h4 fw-bold text-primary">{{ thisItem.uniqueID }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Name -->
|
||||
<div class="col-12 mb-3">
|
||||
<p class="h5 fw-bold">
|
||||
<i class="fas fa-tag me-2 text-secondary"></i>Item Name:
|
||||
<span class="text-muted">{{ thisItem.productName }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Part Number -->
|
||||
<div class="col-12 mb-3">
|
||||
<p class="h5 fw-bold">
|
||||
<i class="fas fa-barcode me-2 text-secondary"></i>Part Number:
|
||||
<span class="text-muted">{{ thisItem.partNumber }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Serial Number -->
|
||||
<div class="col-12 mb-3">
|
||||
<p class="h5 fw-bold">
|
||||
<i class="fas fa-hashtag me-2 text-secondary"></i>Serial Number:
|
||||
<span class="text-muted">{{ thisItem.serialNumber }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Station -->
|
||||
<div class="col-12 mb-3">
|
||||
<p class="h5 fw-bold">
|
||||
<i class="fas fa-user-tie me-2 text-secondary"></i>PIC:
|
||||
<span class="text-muted">{{thisItem.currentUserFullName}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Station -->
|
||||
<div class="col-12 mb-3">
|
||||
<p class="h5 fw-bold">
|
||||
<i class="fas fa-user-tie me-2 text-secondary"></i>Station:
|
||||
<span class="text-muted">{{thisItem.currentStation || 'No Station Deploy (Self Assign)' }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!--RECIEVE INTERFACE -->
|
||||
<div class="col-12" v-if="displayStatus === 'arrived'">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-4 text-primary">
|
||||
<i class="fas fa-info-circle me-2"></i>Receiver Information
|
||||
</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
<!-- User -->
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<span class="fw-bold">
|
||||
<i class="fas fa-user me-2 text-secondary"></i>User:
|
||||
</span>
|
||||
<span class="text-muted text-end" style="max-width: 70%; word-wrap: break-word;">
|
||||
{{ thisItem.currentUser }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Station -->
|
||||
<!-- <li class="list-group-item d-flex justify-content-between align-items-center"> -->
|
||||
<!-- <span class="fw-bold"> -->
|
||||
<!-- <i class="fas fa-map-marker-alt me-2 text-secondary"></i>Station: -->
|
||||
<!-- </span> -->
|
||||
<!-- <span class="text-muted">{{ thisItem.currentStation || 'N/A' }}</span> -->
|
||||
<!-- </li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--RETURN INTERFACE -->
|
||||
<div class="col-12" v-if="displayStatus === 'return'">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-4 text-primary">
|
||||
<i class="fas fa-info-circle me-2"></i>Sender Information
|
||||
</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
<!-- User -->
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<span class="fw-bold">
|
||||
<i class="fas fa-user me-2 text-secondary"></i>User:
|
||||
</span>
|
||||
<span class="text-muted text-end" style="max-width: 70%; word-wrap: break-word;">
|
||||
{{ thisItem.currentUser }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!--RECIEVE INTERFACE -->
|
||||
<form v-on:submit.prevent="updateItemMovement" v-if="displayStatus === 'arrived'" data-aos="fade-right">
|
||||
<div class=" register" data-aos="fade-right">
|
||||
<div class="row" data-aos="fade-right">
|
||||
|
||||
@*Right Side*@
|
||||
<div class="col-md-12">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<br><br>
|
||||
|
||||
@* Submit and Reset Buttons *@
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-8 offset-sm-5">
|
||||
<button type="button" v-on:click="receiveReturnMessage" class="btn btn-secondary m-1">Return Item</button>
|
||||
<button type="submit" class="btn btn-primary m-1">Receive Item</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!--RETURN INTERFACE -->
|
||||
<form v-on:submit.prevent="" v-if="displayStatus === 'return'" data-aos="fade-right">
|
||||
<div class=" register" data-aos="fade-right">
|
||||
<div class="row" data-aos="fade-right">
|
||||
|
||||
@*Right Side*@
|
||||
<div class="col-md-12">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<br><br>
|
||||
@* Submit and Reset Buttons *@
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-8 offset-sm-5">
|
||||
<button type="submit" v-on:click="ReturnMessage" class="btn btn-primary m-1">Return Item</button>
|
||||
<button type="submit" v-on:click="StationMessage" class="btn btn-primary m-1">{{ thisItem?.currentStationId == null ? "Deploy Station" : "Change Station" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!--STATION DEPLOY MESSAGE BOX INTERFACE-->
|
||||
<div class="modal fade" id="stationMessage" tabindex="-1" role="dialog" aria-labelledby="stationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="stationModalLabel">{{ thisItem?.currentStationId == null ? "Deploy Station" : "Change Station" }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" v-on:click="closeStationMessageModal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form v-on:submit.prevent="updateStationItemMovement">
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Deploy Station : </label>
|
||||
<div class="col-sm-8">
|
||||
<select class="btn btn-primary dropdown-toggle col-md-10" v-model="selectedStation">
|
||||
<option class="btn-light" value="" disabled selected>Select Station</option>
|
||||
<option v-if="stationList.length === 0" class="btn-light" disabled>No Station Assigned to You</option>
|
||||
<option class="btn-light" v-for="(station, index) in stationList" :key="index" :value="station.stationId">{{ station.stationName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Remark:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" v-model="remark" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Consignment Note:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="file" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Deploy Station</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--RETURN MESSAGE BOX INTERFACE-->
|
||||
<div class="modal fade" id="returnMessage" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="returnModalLabel">Return Item</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" v-on:click="closeMessageModal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form v-on:submit.prevent="returnItemMovement">
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Remark:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" v-model="remark" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Consignment Note:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="file" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Return Item</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--RECEIVE THEN RETURN INTERFACE -->
|
||||
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="returnModalLabel">Return Item</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" v-on:click="closeModal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form v-on:submit.prevent="receiveReturnAPI">
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Remark:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" v-model="remark" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Consignment Note:</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="file" class="form-control-file" v-on:change="handleFileUpload" accept="image/png, image/jpeg, application/pdf" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Return Item</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ALREADY RETURN ITEM INTERFACE -->
|
||||
<div v-if="displayStatus === 'requestAgain'" class="d-flex justify-content-center align-items-center vh-100">
|
||||
<div class="col-lg-10 col-11 border rounded p-5 shadow-lg text-center min-vh-50">
|
||||
<h1 class="text-danger">The item has been register as returned.</h1>
|
||||
<h3 class="text-muted">You need to request this item again to used it legally.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NOT SIGN IN ITEM INTERFACE -->
|
||||
<div v-if="displayStatus === 'differentUser'" class="d-flex justify-content-center align-items-center vh-100">
|
||||
<div class="col-lg-10 col-11 border rounded p-5 shadow-lg text-center min-vh-50">
|
||||
<h1 class="text-danger">The item is not assigned to you.</h1>
|
||||
<h3 class="text-muted">You need to request this item to validly use it.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script src="~/js/vue-qrcode-reader.umd.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
const app = Vue.createApp({
|
||||
|
||||
data() {
|
||||
return {
|
||||
thisItem: null,
|
||||
selectedUser: "",
|
||||
selectedStation: "",
|
||||
stationList: [],
|
||||
displayStatus: null,
|
||||
debounceTime: 500,
|
||||
currentUser: null,
|
||||
movementId: null,
|
||||
currentUserId: null,
|
||||
remark: null,
|
||||
consignmentNote: null,
|
||||
receiveReturn: null,
|
||||
UniqueID: null,
|
||||
InventoryMasterId: null,
|
||||
|
||||
//QR VARIABLE
|
||||
qrCodeResult: null,
|
||||
debounceTimeout: null,
|
||||
error: "",
|
||||
selectedConstraints: { facingMode: "user" },
|
||||
trackFunctionSelected: { text: 'outline', value: null },
|
||||
barcodeFormats: {
|
||||
qr_code: true, // Hanya mendukung QR Code
|
||||
code_128: true,
|
||||
ean_13: true
|
||||
},
|
||||
constraintOptions: [
|
||||
{ label: "Rear Camera", constraints: { facingMode: "environment" } },
|
||||
{ label: "Front Camera", constraints: { facingMode: "user" } }
|
||||
],
|
||||
videoInputDevices: [],
|
||||
selectedCameraId: null,
|
||||
scanStartTime: null,
|
||||
scanTime: null
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.trackFunctionSelected.value = this.paintOutline;
|
||||
await this.fetchUser();
|
||||
await Promise.all([
|
||||
this.fetchStation(),
|
||||
]);
|
||||
},
|
||||
methods: {
|
||||
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.consignmentNote = e.target.result.split(',')[1]; // Get Base64 string (remove metadata)
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
this.consignmentNote = null;
|
||||
}
|
||||
},
|
||||
|
||||
async updateStationItemMovement() {
|
||||
const requiredFields = ['selectedStation'];
|
||||
|
||||
for (let field of requiredFields) {
|
||||
if (!this[field]) {
|
||||
alert(`Request Error: Please fill in required field ${field}.`, 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const formData = {
|
||||
ItemId: this.thisItem.itemID,
|
||||
ToStation: this.thisItem.currentStationId,
|
||||
ToStore: this.thisItem.toStore,
|
||||
ToUser: this.currentUserId,
|
||||
ToOther: "Delivered",
|
||||
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||
Action: "Assign",
|
||||
Quantity: this.thisItem.quantity,
|
||||
Remark: this.remark,
|
||||
ConsignmentNote: this.consignmentNote,
|
||||
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||
LastUser: this.currentUserId,
|
||||
LastStore: this.thisItem.toStore,
|
||||
LastStation: this.selectedStation,
|
||||
LatestStatus: "Delivered",
|
||||
ReceiveDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||
MovementComplete: true,
|
||||
};
|
||||
|
||||
const response = await fetch('/InvMainAPI/StationItemMovementUser', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.thisItem = await response.json();
|
||||
this.fetchItem(this.thisItem.uniqueID);
|
||||
alert('Success! Item assign to the Station.');
|
||||
$('#stationMessage').modal('hide');
|
||||
this.displayStatus = "return";
|
||||
} else {
|
||||
throw new Error('Failed to submit form.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Inventory PSTW Error: An error occurred.');
|
||||
}
|
||||
},
|
||||
|
||||
async updateItemMovement() {
|
||||
|
||||
if (this.receiveReturn == null) {
|
||||
if (!confirm("Are you sure you already received this item?")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const formData = {
|
||||
Id: this.thisItem.id,
|
||||
LastStore: this.thisItem.toStore,
|
||||
LatestStatus: "Delivered",
|
||||
ReceiveDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||
MovementComplete: true,
|
||||
};
|
||||
|
||||
const response = await fetch('/InvMainAPI/UpdateItemMovementUser', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
if (this.receiveReturn == null) {
|
||||
alert('Success! Item has been successfully received.');
|
||||
this.thisItem = await response.json();
|
||||
this.displayStatus = "return";
|
||||
this.fetchItem(this.UniqueID);
|
||||
this.resetForm();
|
||||
} else {
|
||||
this.returnItemMovement();
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to submit form.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Inventory PSTW Error: An error occurred.');
|
||||
}
|
||||
},
|
||||
|
||||
async returnItemMovement() {
|
||||
|
||||
// const requiredFields = ['remark', 'consignmentNote'];
|
||||
|
||||
// for (let field of requiredFields) {
|
||||
// if (!this[field]) {
|
||||
// alert(`Request Error: Please fill in required field ${field}.`, 'warning');
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!confirm("Are you sure you want to return this item?")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const formData = {
|
||||
ItemId: this.thisItem.itemID,
|
||||
ToStation: this.thisItem.currentStationId,
|
||||
ToStore: this.thisItem.currentStoreId,
|
||||
ToUser: this.currentUserId,
|
||||
ToOther: "Return",
|
||||
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||
Action: "StockIn",
|
||||
Quantity: this.thisItem.quantity,
|
||||
Remark: this.remark,
|
||||
ConsignmentNote: this.consignmentNote,
|
||||
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||
LastUser: this.InventoryMasterId,
|
||||
LastStore: this.thisItem.toStore,
|
||||
LastStation: this.thisItem.toStation,
|
||||
LatestStatus: null,
|
||||
ReceiveDate: null,
|
||||
MovementComplete: false,
|
||||
};
|
||||
|
||||
const response = await fetch('/InvMainAPI/ReturnItemMovementUser', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Success! Item is on the delivery to return to Inventory Master.');
|
||||
this.thisItem = await response.json();
|
||||
$('#returnModal').modal('hide');
|
||||
$('#returnMessage').modal('hide');
|
||||
this.displayStatus = "requestAgain";
|
||||
this.resetForm();
|
||||
} else {
|
||||
throw new Error('Failed to submit form.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Inventory PSTW Error: An error occurred.');
|
||||
}
|
||||
},
|
||||
async fetchItem(itemid) {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/GetItem/' + itemid, {
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
this.thisItem = await response.json();
|
||||
this.fetchStore(this.thisItem.toStore);
|
||||
|
||||
if (this.thisItem.movementId != null && this.thisItem.toOther === "On Delivery" && this.thisItem.latestStatus == null && this.thisItem.currentUserId == this.currentUserId && this.thisItem.movementComplete == 0) {
|
||||
this.displayStatus = "arrived";
|
||||
} else if (this.thisItem.movementId != null && this.thisItem.latestStatus != null && this.thisItem.currentUserId == this.currentUserId && this.thisItem.latestStatus != "Ready To Deploy") {
|
||||
this.displayStatus = "return";
|
||||
} else if (this.thisItem.movementId != null && this.thisItem.toOther === "Return" && this.thisItem.latestStatus == null && this.thisItem.toUser == this.currentUserId) {
|
||||
this.displayStatus = "requestAgain";
|
||||
} else {
|
||||
this.displayStatus = "differentUser";
|
||||
this.thisItem = null;
|
||||
}
|
||||
} else {
|
||||
this.error = 'Qr Code Not Register to the system';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching item information:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchStation() {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/StationList', {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch suppliers');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.stationList = data.filter(station => station.stationPicID === this.currentUserId);
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchStore(storeId) {
|
||||
try {
|
||||
const response = await fetch('/InvMainAPI/StoreSpecificMaster/' + storeId, {
|
||||
method: 'POST', // Specify the HTTP method
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Store');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.InventoryMasterId = data.userId;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching suppliers:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUser() {
|
||||
try {
|
||||
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentUser = data?.userInfo || null;
|
||||
this.currentUserId = this.currentUser.id;
|
||||
}
|
||||
else {
|
||||
console.error('Failed to fetch user');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
|
||||
resetScanner() {
|
||||
this.displayStatus = null;
|
||||
this.qrCodeResult = null;
|
||||
this.thisItem = null;
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.selectedStation = null;
|
||||
},
|
||||
|
||||
async receiveReturnAPI() {
|
||||
this.receiveReturn = 1;
|
||||
this.updateItemMovement();
|
||||
},
|
||||
|
||||
// Split Url dapatkan unique ID Je
|
||||
onDecode(detectedCodes) {
|
||||
if (detectedCodes.length > 0) {
|
||||
this.qrCodeResult = detectedCodes[0].rawValue; // Ambil URL dari rawValue
|
||||
this.UniqueID = this.qrCodeResult.split('/').pop(); // Ambil UniqueID dari URL
|
||||
this.fetchItem(this.UniqueID);
|
||||
}
|
||||
},
|
||||
|
||||
//Showing Qr Error
|
||||
onError(err) {
|
||||
let message = `[${err.name}]: `;
|
||||
if (err.name === "NotAllowedError") {
|
||||
message += "You have to allow camera accecss.";
|
||||
} else if (err.name === "NotFoundError") {
|
||||
message += "There's no camera detect.";
|
||||
} else if (err.name === "NotReadableError") {
|
||||
message += "You are using camera on the other application.";
|
||||
} else {
|
||||
message += err.message;
|
||||
}
|
||||
this.error = message;
|
||||
},
|
||||
|
||||
//Setting Camera to know that camera are turn on or not
|
||||
async onCameraReady(videoElement) {
|
||||
|
||||
const video = document.querySelector("video");
|
||||
|
||||
const track = video.srcObject.getVideoTracks()[0];
|
||||
|
||||
if (track && track.getCapabilities) {
|
||||
const capabilities = track.getCapabilities(); // Get camera capabilities
|
||||
|
||||
if (capabilities.sharpness) {
|
||||
track.applyConstraints({
|
||||
advanced: [{ width: 1280, height: 720 }]
|
||||
}).then(() => {
|
||||
|
||||
// Step 2: Apply sharpness separately
|
||||
return track.applyConstraints({ advanced: [{ sharpness: 10 }] });
|
||||
}).then(() => {
|
||||
|
||||
return track.applyConstraints({ advanced: [{ exposureMode: 'continuous' }] });
|
||||
})
|
||||
.then(() => {
|
||||
}).catch(err => console.error("Failed to apply constraints:", err));
|
||||
|
||||
} else {
|
||||
console.warn("⚠️ Sharpness not supported on this camera");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
this.videoInputDevices = devices.filter(device => device.kind === 'videoinput');
|
||||
|
||||
if (this.videoInputDevices.length > 0) {
|
||||
// Keep the selected camera if already chosen
|
||||
if (!this.selectedCameraId) {
|
||||
this.selectedCameraId = this.videoInputDevices[0].deviceId;
|
||||
}
|
||||
|
||||
this.selectedConstraints = { deviceId: { exact: this.selectedCameraId } };
|
||||
} else {
|
||||
this.error = "No camera detected.";
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = "Error accessing camera: " + err.message;
|
||||
}
|
||||
},
|
||||
|
||||
//Update Camera Category
|
||||
updateCamera() {
|
||||
this.selectedConstraints = { deviceId: { exact: this.selectedCameraId } };
|
||||
},
|
||||
|
||||
//Red box if QR Detect
|
||||
paintOutline(detectedCodes, ctx) {
|
||||
for (const detectedCode of detectedCodes) {
|
||||
const [firstPoint, ...otherPoints] = detectedCode.cornerPoints;
|
||||
|
||||
ctx.strokeStyle = 'red'; // Warna garis merah
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(firstPoint.x, firstPoint.y);
|
||||
for (const { x, y } of otherPoints) {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo(firstPoint.x, firstPoint.y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
|
||||
//Ni return message
|
||||
receiveReturnMessage() {
|
||||
$("#returnModal").modal("show");
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
$('#returnModal').modal('hide'); // Manually hide the modal
|
||||
},
|
||||
|
||||
ReturnMessage() {
|
||||
$("#returnMessage").modal("show");
|
||||
},
|
||||
|
||||
closeMessageModal() {
|
||||
$('#returnMessage').modal('hide'); // Manually hide the modal
|
||||
},
|
||||
|
||||
StationMessage() {
|
||||
$("#stationMessage").modal("show");
|
||||
},
|
||||
|
||||
closeStationMessageModal() {
|
||||
$('#stationMessage').modal('hide'); // Manually hide the modal
|
||||
},
|
||||
},
|
||||
});
|
||||
app.component("qrcode-stream", VueQrcodeReader.QrcodeStream);
|
||||
|
||||
app.mount('#registerItem');
|
||||
|
||||
$(function () {
|
||||
// Attach event listener to show modal
|
||||
$('#addItemBtn').on('click', function () {
|
||||
$('#registerItemModal').modal('show');
|
||||
});
|
||||
|
||||
// Close modals
|
||||
$('.closeModal').on('click', function () {
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
90
Areas/Inventory/Views/ItemMovement/UserDashboard.cshtml
Normal file
90
Areas/Inventory/Views/ItemMovement/UserDashboard.cshtml
Normal file
@ -0,0 +1,90 @@
|
||||
@{
|
||||
ViewData["Title"] = "Dashboard";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
<div class="container" id="invUser">
|
||||
<div class="row">
|
||||
<div class="text-center">
|
||||
<p><h1 class="display-4">Inventory User Dashboard</h1></p>
|
||||
<p v-show="currentUserCompanyDept.departmentName"><h2 class="display-6">Store: {{ currentUserCompanyDept.departmentName }}</h2></p>
|
||||
</div>
|
||||
</div>
|
||||
@await Html.PartialAsync("~/Areas/Inventory/Views/_InventoryPartialUser.cshtml")
|
||||
</div>
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
<script>
|
||||
$(function () {
|
||||
app.mount('#invUser');
|
||||
|
||||
$('.closeModal').on('click', function () {
|
||||
// Show the modal with the ID 'addManufacturerModal'.
|
||||
$('.modal').modal('hide');
|
||||
});
|
||||
});
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
currentUser: null,
|
||||
currentUserCompanyDept: {
|
||||
departmentName: null,
|
||||
departmentId: null
|
||||
},
|
||||
reportData: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUser();
|
||||
},
|
||||
methods: {
|
||||
async fetchUser() {
|
||||
try {
|
||||
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentUser = data?.userInfo || null;
|
||||
const companyDeptData = await this.currentUser.department;
|
||||
const userRole = this.currentUser.role;
|
||||
if(userRole == "SuperAdmin" || userRole == "SystemAdmin"){
|
||||
this.currentUserCompanyDept = {departmentId : 0, departmentName : "All"}
|
||||
this.fetchInventoryReport(0);
|
||||
}
|
||||
else{
|
||||
this.currentUserCompanyDept = companyDeptData;
|
||||
this.fetchInventoryReport(companyDeptData.departmentId);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
async fetchInventoryReport(deptId){
|
||||
try {
|
||||
const response = await fetch(`/InvMainAPI/GetInventoryReport/` + deptId, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.reportData = data;
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
}
|
||||
11
Areas/Inventory/Views/Main/Index.cshtml
Normal file
11
Areas/Inventory/Views/Main/Index.cshtml
Normal file
@ -0,0 +1,11 @@
|
||||
@{
|
||||
ViewData["Title"] = "PSTW Centralized System";
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Welcome To Invetory Module</h1>
|
||||
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user