Merge pull request #345 from LD-Reborn/340-chore-wcag-22-aa-check

340 chore wcag 22 aa check
This commit is contained in:
LD50
2025-11-29 13:14:02 +01:00
committed by GitHub
32 changed files with 1023 additions and 414 deletions

View File

@@ -98,7 +98,6 @@ public class HomeController : Controller
[ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "uid", "size" })] [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "uid", "size" })]
public async Task<IActionResult> UserPhotoAsync(string uid, int? size) public async Task<IActionResult> UserPhotoAsync(string uid, int? size)
{ {
Task<AdminSettingsModel> adminSettingsModelTask = _ldap.GetAdminSettingsModelAsync();
UserModel? user = await _ldap.GetUserByUidAsync(uid, _ldap.UsersAttributes); UserModel? user = await _ldap.GetUserByUidAsync(uid, _ldap.UsersAttributes);
if (user is null || user.JpegPhoto is null || user.JpegPhoto == "") if (user is null || user.JpegPhoto is null || user.JpegPhoto == "")
{ {
@@ -110,7 +109,7 @@ public class HomeController : Controller
} }
if (size is not null) if (size is not null)
{ {
AdminSettingsModel adminSettingsModel = await adminSettingsModelTask; AdminSettingsModel adminSettingsModel = await _ldap.GetAdminSettingsModelAsync();
size = Math.Min((int)size, adminSettingsModel.MaxDownloadableUserImageSize); size = Math.Min((int)size, adminSettingsModel.MaxDownloadableUserImageSize);
} }
byte[] encodedFile = ImageHelper.ResizeAndConvertToWebp(user.JpegPhoto, size ?? 32); byte[] encodedFile = ImageHelper.ResizeAndConvertToWebp(user.JpegPhoto, size ?? 32);
@@ -167,6 +166,7 @@ public class HomeController : Controller
return RedirectToAction("Index", "Home"); return RedirectToAction("Index", "Home");
} }
Response.StatusCode = 500;
switch (authenticationResult.AuthenticationState) switch (authenticationResult.AuthenticationState)
{ {
case UserNotAuthenticatedReason.InvalidCredentials: case UserNotAuthenticatedReason.InvalidCredentials:
@@ -194,9 +194,29 @@ public class HomeController : Controller
return RedirectToAction("Index", "Home"); return RedirectToAction("Index", "Home");
} }
[HttpGet("Accessibility")]
public ActionResult Accessibility()
{
return View();
}
[HttpGet("AccessDenied")] [HttpGet("AccessDenied")]
public ActionResult AccessDenied() public ActionResult AccessDenied()
{ {
return View(); return View();
} }
[Authorize]
[HttpGet("RemainingTime")]
public async Task<IActionResult> GetRemainingSessionTime()
{
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!result.Succeeded || result.Properties?.ExpiresUtc == null)
return Json(new { remainingMinutes = 0 });
var remaining = result.Properties.ExpiresUtc.Value - DateTimeOffset.UtcNow;
return Json(new { remainingMinutes = (int) Math.Ceiling(remaining.TotalMinutes) });
}
} }

View File

@@ -76,7 +76,6 @@ public class UsersController : Controller
UserDescription? description = requestModel.Description; UserDescription? description = requestModel.Description;
jpegPhoto ??= ImageHelper.GetDefaultUserImageAsBase64(); jpegPhoto ??= ImageHelper.GetDefaultUserImageAsBase64();
string uid = UsersHelper.CreateUsername(requestModel.Cn ?? "", requestModel.Sn ?? ""); string uid = UsersHelper.CreateUsername(requestModel.Cn ?? "", requestModel.Sn ?? "");
title ??= "";
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []}; description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
if (!userPassword.StartsWith('{')) if (!userPassword.StartsWith('{'))
{ {
@@ -88,12 +87,13 @@ public class UsersController : Controller
new LdapAttribute("objectClass", "inetOrgPerson"), new LdapAttribute("objectClass", "inetOrgPerson"),
new LdapAttribute("cn", requestModel.Cn), new LdapAttribute("cn", requestModel.Cn),
new LdapAttribute("sn", requestModel.Sn), new LdapAttribute("sn", requestModel.Sn),
new LdapAttribute("title", title),
new LdapAttribute("uid", uid), new LdapAttribute("uid", uid),
new LdapAttribute("jpegPhoto", jpegPhoto), new LdapAttribute("jpegPhoto", jpegPhoto),
new LdapAttribute("description", JsonSerializer.Serialize(description)), new LdapAttribute("description", JsonSerializer.Serialize(description)),
new LdapAttribute("userPassword", userPassword), new LdapAttribute("userPassword", userPassword),
]; ];
if (title is not null && title.Length > 0) attributeSet.Add(new LdapAttribute("title", title));
await _ldap.CreateUser(uid, attributeSet); await _ldap.CreateUser(uid, attributeSet);
return new(){Success = true, Uid = uid}; return new(){Success = true, Uid = uid};
} }
@@ -122,7 +122,7 @@ public class UsersController : Controller
await _ldap.UpdateUser(uid, "uid", requestModel.NewUid); await _ldap.UpdateUser(uid, "uid", requestModel.NewUid);
uid = requestModel.NewUid; uid = requestModel.NewUid;
} }
if (requestModel.Title is not null) if (requestModel.Title is not null && requestModel.Title.Length > 0)
{ {
await _ldap.UpdateUser(uid, "title", requestModel.Title); await _ldap.UpdateUser(uid, "title", requestModel.Title);
} }

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
</resheader>
<data name="Accessibility" xml:space="preserve">
<value>Barrierefreiheit</value>
</data>
<data name="PageIntro" xml:space="preserve">
<value>Am {0} entspricht sämtlicher Inhalt im Projekt LD-Reborn/HAM den Web Content Accessibility Guidelines 2.2 unter {1}. Konformitätsstufe Triple-A.</value>
</data>
<data name="ReliesUpon" xml:space="preserve">
<value>Die Technologie, auf die sich dieser Inhalt "<a>stützt</a>", ist:</value>
</data>
<data name="UsesButNotReliesUpon" xml:space="preserve">
<value>Die Technologien, die dieser Inhalt <strong>verwendet, jedoch nicht voraussetzt</strong>, sind:</value>
</data>
<data name="TestedWith" xml:space="preserve">
<value>Dieser Inhalt wurde mit den folgenden User Agents und Assistenztechnologien getestet:</value>
</data>
<data name="Features" xml:space="preserve">
<value>Diese Seite enthält sowohl <span property="accessMode" content="textual">Text</span>
als auch <span property="accessMode" content="visual">Bilder</span>.
<span property="accessibilityFeature" content="alternativeText">Alternativtexte</span> sind für alle Bilder enthalten, und <span
property="accessibilityFeature" content="longDescription">ausführliche Beschreibungen</span> werden für Bilder bereitgestellt, die mehr als einfachen Alternativtext erfordern. Sämtliche Inhalte sind in Textform verfügbar und können von Assistenztechnologien ausgelesen werden.</value>
</data>
<data name="OnWith" xml:space="preserve">
<value>{0} mit {1} und {2}</value>
</data>
</root>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
</resheader>
<data name="Accessibility" xml:space="preserve">
<value>Accessibility</value>
</data>
<data name="PageIntro" xml:space="preserve">
<value>On {0}, all content available in the project LD-Reborn/HAM conforms to Web Content Accessibility Guidelines 2.2 at {1}. Triple-A conformance.</value>
</data>
<data name="ReliesUpon" xml:space="preserve">
<value>The technology that this content "<a>relies upon</a>" is:</value>
</data>
<data name="UsesButNotReliesUpon" xml:space="preserve">
<value>The technologies that this content "<strong>uses but does not rely
upon</strong>" are:</value>
</data>
<data name="TestedWith" xml:space="preserve">
<value>This content was tested using the following user agents and assistive
technologies:</value>
</data>
<data name="Features" xml:space="preserve">
<value>This page includes both <span property="accessMode" content="textual">text</span>
and <span property="accessMode" content="visual">images</span>.
<span property="accessibilityFeature" content="alternativeText">Alternative
text</span> is included for all image content and <span
property="accessibilityFeature" content="longDescription">long
descriptions</span> are also provided for images that require more
than simple alternate text. All content is available in text, which
can be accessed by assistive technology.</value>
</data>
<data name="OnWith" xml:space="preserve">
<value>{0} on {1} with {2}</value>
</data>
</root>

View File

@@ -184,4 +184,7 @@
<data name="Apply preset" xml:space="preserve"> <data name="Apply preset" xml:space="preserve">
<value>Vorlage anwenden</value> <value>Vorlage anwenden</value>
</data> </data>
<data name="Jump to add attribute button" xml:space="preserve">
<value>Zu Attribut Hinzufügen Button springen</value>
</data>
</root> </root>

View File

@@ -127,4 +127,10 @@
<data name="Error updating group" xml:space="preserve"> <data name="Error updating group" xml:space="preserve">
<value>Fehler beim Anpassen der Gruppe</value> <value>Fehler beim Anpassen der Gruppe</value>
</data> </data>
<data name="Yes" xml:space="preserve">
<value>Ja</value>
</data>
<data name="No" xml:space="preserve">
<value>Nein</value>
</data>
</root> </root>

View File

@@ -40,5 +40,10 @@
<data name="Navigate from here or the navigation bar" xml:space="preserve"> <data name="Navigate from here or the navigation bar" xml:space="preserve">
<value>Navigiere von hier oder über die Navigationsleiste</value> <value>Navigiere von hier oder über die Navigationsleiste</value>
</data> </data>
<data name="User settings" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="Admin settings" xml:space="preserve">
<value>Administration</value>
</data>
</root> </root>

View File

@@ -150,4 +150,7 @@
<data name="Successfully added barcode to print batch" xml:space="preserve"> <data name="Successfully added barcode to print batch" xml:space="preserve">
<value>Barcode wurde erfolgreich dem Auftragsstapel hinzugefügt</value> <value>Barcode wurde erfolgreich dem Auftragsstapel hinzugefügt</value>
</data> </data>
<data name="Jump to add attribute button" xml:space="preserve">
<value>Zu Attribut Hinzufügen Button springen</value>
</data>
</root> </root>

View File

@@ -19,12 +19,6 @@
<data name="Login" xml:space="preserve"> <data name="Login" xml:space="preserve">
<value>Anmelden</value> <value>Anmelden</value>
</data> </data>
<data name="Username" xml:space="preserve">
<value>Benutzername</value>
</data>
<data name="Password" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="Invalid login credentials" xml:space="preserve"> <data name="Invalid login credentials" xml:space="preserve">
<value>Ungültige Anmeldedaten</value> <value>Ungültige Anmeldedaten</value>
</data> </data>

View File

@@ -110,4 +110,10 @@
<data name="Clear user image cache" xml:space="preserve"> <data name="Clear user image cache" xml:space="preserve">
<value>Cache leeren</value> <value>Cache leeren</value>
</data> </data>
<data name="Presets. Press to toggle visibility" xml:space="preserve">
<value>Vorlagen. Klicken zum Auf- oder zuklappen</value>
</data>
<data name="Skip attributes table" xml:space="preserve">
<value>Attribut-Tabelle überspringen</value>
</data>
</root> </root>

View File

@@ -64,4 +64,7 @@
<data name="Error contacting server" xml:space="preserve"> <data name="Error contacting server" xml:space="preserve">
<value>Fehler bei der Kommunikation mit dem Server</value> <value>Fehler bei der Kommunikation mit dem Server</value>
</data> </data>
<data name="There are no changes to be saved." xml:space="preserve">
<value>Es wurden keine Änderungen gemacht.</value>
</data>
</root> </root>

View File

@@ -73,4 +73,7 @@
<data name="Serial" xml:space="preserve"> <data name="Serial" xml:space="preserve">
<value>Seriennummer</value> <value>Seriennummer</value>
</data> </data>
<data name="Delete entry" xml:space="preserve">
<value>Eintrag löschen</value>
</data>
</root> </root>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
</resheader>
<data name="Open Print Page" xml:space="preserve">
<value>Druckseite öffnen</value>
</data>
</root>

View File

@@ -37,6 +37,9 @@
<data name="Login" xml:space="preserve"> <data name="Login" xml:space="preserve">
<value>Anmelden</value> <value>Anmelden</value>
</data> </data>
<data name="Accessibility" xml:space="preserve">
<value>Barrierefreiheit</value>
</data>
<data name="Select location" xml:space="preserve"> <data name="Select location" xml:space="preserve">
<value>Ort auswählen</value> <value>Ort auswählen</value>
</data> </data>
@@ -52,10 +55,37 @@
<data name="Select preset" xml:space="preserve"> <data name="Select preset" xml:space="preserve">
<value>Vorlage auswählen</value> <value>Vorlage auswählen</value>
</data> </data>
<data name="Close alert" xml:space="preserve">
<value>Meldung schließen</value>
</data>
<data name="User settings" xml:space="preserve"> <data name="User settings" xml:space="preserve">
<value>Einstellungen</value> <value>Einstellungen</value>
</data> </data>
<data name="Admin settings" xml:space="preserve"> <data name="Admin settings" xml:space="preserve">
<value>Administration</value> <value>Administration</value>
</data> </data>
<data name="Extend session duration" xml:space="preserve">
<value>Sitzung verlängern</value>
</data>
<data name="The session expires soon." xml:space="preserve">
<value>Die Sitzung läuft bald ab.</value>
</data>
<data name="Please authenticate to continue without losing data." xml:space="preserve">
<value>Bitte authentifizieren Sie sich, um fortzufahren, ohne Daten zu verlieren.</value>
</data>
<data name="Re-authenticate" xml:space="preserve">
<value>Authentifizieren</value>
</data>
<data name="Invalid login credentials" xml:space="preserve">
<value>Ungültige Anmeldedaten</value>
</data>
<data name="Your account has been locked. Wait a few minutes or ask an administrator to unlock you" xml:space="preserve">
<value>Ihr Konto wurde gesperrt. Warten Sie einige Minuten oder bitten Sie einen Administrator, die Sperre aufzuheben.</value>
</data>
<data name="You are not authorized for login. Ask an administrator to authorize you." xml:space="preserve">
<value>Sie sind nicht zur Anmeldung berechtigt. Bitten Sie einen Administrator, Ihnen die Berechtigung zu erteilen.</value>
</data>
<data name="Hell froze over. Make a screenshot and send it to an administrator." xml:space="preserve">
<value>Die Hölle ist zugefroren. Machen Sie einen Screenshot und senden Sie ihn an einen Administrator.</value>
</data>
</root> </root>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
</resheader>
<data name="Login" xml:space="preserve">
<value>Anmelden</value>
</data>
<data name="Username" xml:space="preserve">
<value>Benutzername</value>
</data>
<data name="Password" xml:space="preserve">
<value>Passwort</value>
</data>
</root>

View File

@@ -11,6 +11,7 @@ public partial class LdapService : IDisposable
{ {
private readonly LdapConfig _opts; private readonly LdapConfig _opts;
private readonly LdapConnection _conn; private readonly LdapConnection _conn;
private readonly SemaphoreSlim _connLock = new(1, 1);
private AdminSettingsModel? adminSettingsModel; private AdminSettingsModel? adminSettingsModel;
private ILogger _logger; private ILogger _logger;
@@ -27,6 +28,9 @@ public partial class LdapService : IDisposable
int retries = 0; int retries = 0;
while (retries++ < _opts.ConnectionRetryCount) while (retries++ < _opts.ConnectionRetryCount)
{ {
try
{
await _connLock.WaitAsync();
try try
{ {
if (!_conn.Connected) if (!_conn.Connected)
@@ -42,6 +46,11 @@ public partial class LdapService : IDisposable
} }
} }
await _conn.BindAsync(_opts.BindDn, _opts.BindPassword); await _conn.BindAsync(_opts.BindDn, _opts.BindPassword);
}
finally
{
_connLock.Release();
}
return; return;
} }
catch (Exception ex) catch (Exception ex)
@@ -109,7 +118,7 @@ public partial class LdapService : IDisposable
LdapModification.Replace, LdapModification.Replace,
new LdapAttribute("description", targetText) new LdapAttribute("description", targetText)
); );
await _conn.ModifyAsync(dn, modification); await ModifyAsync(dn, modification);
} }
catch (Exception) catch (Exception)
{ {
@@ -151,7 +160,7 @@ public partial class LdapService : IDisposable
LdapModification.Replace, LdapModification.Replace,
new LdapAttribute("description", targetText) new LdapAttribute("description", targetText)
); );
await _conn.ModifyAsync(dn, modification); await ModifyAsync(dn, modification);
} }
catch (Exception) catch (Exception)
{ {
@@ -201,7 +210,7 @@ public partial class LdapService : IDisposable
LdapModification.Replace, LdapModification.Replace,
new LdapAttribute("description", targetText) new LdapAttribute("description", targetText)
); );
await _conn.ModifyAsync(dn, modification); await ModifyAsync(dn, modification);
} }
catch (Exception) catch (Exception)
{ {
@@ -387,16 +396,17 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
} }
public async Task<IEnumerable<Dictionary<string, string>>> ListObjectBy(string baseDn, string filter, string[] attributes) public async Task<IEnumerable<Dictionary<string, string>>> ListObjectBy(string baseDn, string filter, string[] attributes)
{
return await Task.Run(async () =>
{ {
await ConnectAndBind(); await ConnectAndBind();
await _connLock.WaitAsync();
try
{
var search = await _conn.SearchAsync( var search = await _conn.SearchAsync(
baseDn, baseDn,
LdapConnection.ScopeSub, LdapConnection.ScopeSub,
$"{filter}", $"{filter}",
attributes, attributes,
false); false).ConfigureAwait(false);
var list = new List<Dictionary<string, string>>(); var list = new List<Dictionary<string, string>>();
while (await search.HasMoreAsync()) while (await search.HasMoreAsync())
{ {
@@ -415,7 +425,11 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
catch (LdapException) { } catch (LdapException) { }
} }
return list; return list;
}); }
finally
{
_connLock.Release();
}
} }
public async Task DeleteUserAsync(string uid) public async Task DeleteUserAsync(string uid)
@@ -467,16 +481,24 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
await ConnectAndBind(); await ConnectAndBind();
string dn = PrependRDN($"{rdnKey}={rdnValue}", baseDn); string dn = PrependRDN($"{rdnKey}={rdnValue}", baseDn);
if (attributeName == rdnKey) if (attributeName == rdnKey)
{
await _connLock.WaitAsync();
try
{ {
await _conn.RenameAsync(dn, $"{rdnKey}={attributeValue}", true); await _conn.RenameAsync(dn, $"{rdnKey}={attributeValue}", true);
} }
finally
{
_connLock.Release();
}
}
else else
{ {
var modification = new LdapModification( var modification = new LdapModification(
LdapModification.Replace, LdapModification.Replace,
new LdapAttribute(attributeName, attributeValue) new LdapAttribute(attributeName, attributeValue)
); );
await _conn.ModifyAsync(dn, modification); await ModifyAsync(dn, modification);
} }
} }
@@ -490,24 +512,48 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
new LdapAttribute(attributeName) new LdapAttribute(attributeName)
); );
await _conn.ModifyAsync(dn, modification); await ModifyAsync(dn, modification);
} }
public async Task DeleteObjectByDnAsync(string dn) public async Task DeleteObjectByDnAsync(string dn)
{
await _connLock.WaitAsync();
try
{ {
await _conn.DeleteAsync(dn); await _conn.DeleteAsync(dn);
} }
finally
{
_connLock.Release();
}
}
public async Task CreateObject(string dn, LdapAttributeSet attributeSet) public async Task CreateObject(string dn, LdapAttributeSet attributeSet)
{ {
await ConnectAndBind(); await ConnectAndBind();
LdapEntry ldapEntry = new(dn, attributeSet); LdapEntry ldapEntry = new(dn, attributeSet);
await _connLock.WaitAsync();
try
{
await _conn.AddAsync(ldapEntry); await _conn.AddAsync(ldapEntry);
} }
finally
public async Task ModifyAsync(string dn, LdapModification ldapModification)
{ {
await _conn.ModifyAsync(dn, ldapModification); _connLock.Release();
}
}
public async Task ModifyAsync(string dn, LdapModification mod, CancellationToken ct = default)
{
await _connLock.WaitAsync(ct);
try
{
await _conn.ModifyAsync(dn, mod, ct);
}
finally
{
_connLock.Release();
}
} }
public void Dispose() public void Dispose()

View File

@@ -0,0 +1,22 @@
@using Microsoft.AspNetCore.Html
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer T
@{
ViewData["Title"] = T["Accessibility"];
}
<div typeof="WebPage" vocab="http://schema.org/">
<p property="accessibilitySummary">@T["PageIntro",
"28 November 2025",
new HtmlString("<a href=\"https://www.w3.org/TR/2024/REC-WCAG22-20241212/\">https://www.w3.org/TR/2024/REC-WCAG22-20241212/</a>")
])
</p>
<ul>
<li>@T["ReliesUpon"]
HTML 5.</li>
<li>@T["UsesButNotReliesUpon"] CSS2.1, gif.</li>
<li>@T["TestedWith"] @T["OnWith", "Firefox 145.0.2", "Kubuntu", "Orca"]
</li>
</ul>
<p>@T["Features"]</p>
</div>

View File

@@ -30,17 +30,17 @@
<table class="table table-striped align-middle"> <table class="table table-striped align-middle">
<thead> <thead>
<tr> <tr>
<th>@T["Owner"]</th> <th id="col-owner">@T["Owner"]</th>
<th>@T["Asset ID"]</th> <th id="col-assetId">@T["Asset ID"]</th>
<th>@T["Asset Name"]</th> <th id="col-assetName">@T["Asset Name"]</th>
<th>@T["Location"]</th> <th id="col-assetLocation">@T["Location"]</th>
<th class="text-center">@T["Action"]</th> <th class="text-center">@T["Action"]</th>
</tr> </tr>
<tr> <tr>
<th><input type="text" class="form-control form-control-sm column-filter" placeholder="@T["Owner"]" data-column="0" /></th> <th><input aria-labelledby="col-owner" type="text" class="form-control form-control-sm column-filter" placeholder="@T["Owner"]" data-column="0" /></th>
<th><input type="text" class="form-control form-control-sm column-filter" placeholder="@T["Asset ID"]" data-column="1" /></th> <th><input aria-labelledby="col-assetId" type="text" class="form-control form-control-sm column-filter" placeholder="@T["Asset ID"]" data-column="1" /></th>
<th><input type="text" class="form-control form-control-sm column-filter" placeholder="@T["Asset Name"]" data-column="2" /></th> <th><input aria-labelledby="col-assetName" type="text" class="form-control form-control-sm column-filter" placeholder="@T["Asset Name"]" data-column="2" /></th>
<th><input type="text" class="form-control form-control-sm column-filter" placeholder="@T["Location"]" data-column="3" /></th> <th><input aria-labelledby="col-assetLocation" type="text" class="form-control form-control-sm column-filter" placeholder="@T["Location"]" data-column="3" /></th>
<th class="text-center">-</th> <th class="text-center">-</th>
</tr> </tr>
</thead> </thead>
@@ -48,7 +48,7 @@
@{ @{
foreach (AssetsTableViewModel assetsTableViewModel in Model.AssetsTableViewModels) foreach (AssetsTableViewModel assetsTableViewModel in Model.AssetsTableViewModels)
{ {
<tr class="asset-row" data-asset-id="@assetsTableViewModel.AssetCn"> <tr tabindex="0" role="button" class="asset-row" data-asset-id="@assetsTableViewModel.AssetCn">
<td>@assetsTableViewModel.UserUID</td> <td>@assetsTableViewModel.UserUID</td>
<td>@assetsTableViewModel.AssetCn</td> <td>@assetsTableViewModel.AssetCn</td>
<td>@assetsTableViewModel.AssetName</td> <td>@assetsTableViewModel.AssetName</td>
@@ -228,9 +228,18 @@
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="fw-bold mb-0">@T["Attributes"]</h4> <h4 class="fw-bold mb-0">@T["Attributes"]</h4>
</div> </div>
<div id="attributesContainer" class="d-flex flex-column gap-2"> <a href="#addAttributeBtn" class="visually-hidden-focusable">@T["Jump to add attribute button"]</a>
<!-- Dynamic attribute rows will appear here --> <table class="w-100">
</div> <thead>
<tr>
<th class="visually-hidden">@T["Attribute name"]</th>
<th class="visually-hidden">@T["Attribute value"]</th>
<th class="visually-hidden">@T["Delete"]</th>
</tr>
</thead>
<tbody id="attributesContainer">
</tbody>
</table>
<button type="button" class="btn btn-sm btn-primary mt-3" id="addAttributeBtn"> <button type="button" class="btn btn-sm btn-primary mt-3" id="addAttributeBtn">
@T["Add Attribute"] @T["Add Attribute"]
</button> </button>
@@ -298,12 +307,19 @@ document.addEventListener('DOMContentLoaded', () => {
const addAttributeBtn = document.getElementById('addAttributeBtn'); const addAttributeBtn = document.getElementById('addAttributeBtn');
addAttributeBtn.addEventListener('click', () => { addAttributeBtn.addEventListener('click', () => {
const row = document.createElement('div'); const row = document.createElement('tr');
row.className = 'd-flex gap-2 align-items-center attribute-row'; row.className = 'attribute-row';
row.classList.add("mb-3");
row.innerHTML = ` row.innerHTML = `
<td class="col-md-5 p-1">
<input type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name /> <input type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name />
</td>
<td class="col-md-5 p-1">
<input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value /> <input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button> <button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`; `;
attributesContainer.appendChild(row); attributesContainer.appendChild(row);
}); });
@@ -364,7 +380,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (tableBody) { if (tableBody) {
const newRow = document.createElement('tr'); const newRow = document.createElement('tr');
newRow.innerHTML = ` newRow.innerHTML = `
<td>${jsonData.Owner || ''}</td> <td tabindex="0">${jsonData.Owner || ''}</td>
<td>${result.assetId || ''}</td> <td>${result.assetId || ''}</td>
<td>${jsonData.Name || ''}</td> <td>${jsonData.Name || ''}</td>
<td>${jsonData.Location || ''}</td> <td>${jsonData.Location || ''}</td>
@@ -463,7 +479,18 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="fw-bold mb-0">@T["Attributes"]</h4> <h4 class="fw-bold mb-0">@T["Attributes"]</h4>
</div> </div>
<div id="updateAttributesContainer" class="d-flex flex-column gap-2"></div> <a href="#updateAddAttributeBtn" class="visually-hidden-focusable">@T["Jump to add attribute button"]</a>
<table class="w-100">
<thead>
<tr>
<th class="visually-hidden">@T["Attribute name"]</th>
<th class="visually-hidden">@T["Attribute value"]</th>
<th class="visually-hidden">@T["Delete"]</th>
</tr>
</thead>
<tbody id="updateAttributesContainer">
</tbody>
</table>
<button type="button" class="btn btn-sm btn-primary mt-3" id="updateAddAttributeBtn"> <button type="button" class="btn btn-sm btn-primary mt-3" id="updateAddAttributeBtn">
@T["Add Attribute"] @T["Add Attribute"]
</button> </button>
@@ -510,12 +537,21 @@ document.addEventListener('DOMContentLoaded', () => {
let assetId = null; let assetId = null;
addAttrBtn.addEventListener('click', () => { addAttrBtn.addEventListener('click', () => {
const row = document.createElement('div'); const row = document.createElement('tr');
row.className = 'd-flex gap-2 align-items-center attribute-row'; row.className = 'attribute-row mb-3';
let randomId = crypto.randomUUID();
row.innerHTML = ` row.innerHTML = `
<input type="text" class="form-control" placeholder="@T["Attribute name"]" aria-label="@T["Attribute name"]" data-attr-name /> <td class="col-md-5 p-1">
<input type="text" class="form-control" placeholder="@T["Attribute value"]" aria-label="@T["Attribute value"]" data-attr-value /> <label for="updateName-${randomId}" class="visually-hidden">@T["Attribute name"]</label>
<input id="updateName-${randomId}" type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name />
</td>
<td class="col-md-5 p-1">
<label for="updateValue-${randomId}" class="visually-hidden">@T["Attribute value"]</label>
<input id="updateValue-${randomId}" type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button> <button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`; `;
updateAttributesContainer.appendChild(row); updateAttributesContainer.appendChild(row);
}); });
@@ -559,12 +595,21 @@ document.addEventListener('DOMContentLoaded', () => {
// Attributes // Attributes
if (asset.Description.Attributes) { if (asset.Description.Attributes) {
for (const [attrName, attrValue] of Object.entries(asset.Description.Attributes)) { for (const [attrName, attrValue] of Object.entries(asset.Description.Attributes)) {
const row = document.createElement('div'); const row = document.createElement('tr');
row.className = 'd-flex gap-2 align-items-center attribute-row'; let randomId = crypto.randomUUID();
row.className = 'attribute-row mb-3';
row.innerHTML = ` row.innerHTML = `
<input type="text" class="form-control" value="${attrName}" aria-label="@T["Attribute name"]" data-attr-name /> <td class="col-md-5 p-1">
<input type="text" class="form-control" value="${attrValue}" aria-label="@T["Attribute value"]" data-attr-value /> <label for="updateName-${randomId}" class="visually-hidden">@T["Attribute name"]</label>
<input id="updateName-${randomId}" type="text" class="form-control" value="${attrName}" placeholder="@T["Attribute name"]" data-attr-name />
</td>
<td class="col-md-5 p-1">
<label for="updateValue-${randomId}" class="visually-hidden">@T["Attribute value"]</label>
<input id="updateValue-${randomId}" type="text" class="form-control" value="${attrValue}" placeholder="@T["Attribute value"]" data-attr-value />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button> <button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`; `;
updateAttributesContainer.appendChild(row); updateAttributesContainer.appendChild(row);
} }
@@ -672,7 +717,7 @@ document.addEventListener('DOMContentLoaded', () => {
.asset-row > td { .asset-row > td {
transition: 0.1s ease; transition: 0.1s ease;
} }
.asset-row:has(td:not(:last-child):hover) > td { .asset-row:has(td:not(:last-child):is(:hover, :focus)) > td {
background-color: #17a2b8; background-color: #17a2b8;
} }
</style> </style>
@@ -686,9 +731,17 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
function registerRowDetailviewClick(row) { function registerRowDetailviewClick(row) {
row.addEventListener('click', async (e) => handleRowDetailViewEvent(e, row));
row.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
row.click();
}
});
}
async function handleRowDetailViewEvent(e, row) {
const viewModal = document.getElementById('viewAssetModal'); const viewModal = document.getElementById('viewAssetModal');
const viewContent = document.getElementById('viewAssetContent'); const viewContent = document.getElementById('viewAssetContent');
row.addEventListener('click', async (e) => {
// Avoid clicks on buttons inside the row // Avoid clicks on buttons inside the row
if (e.target.closest('button')) return; if (e.target.closest('button')) return;
@@ -707,12 +760,12 @@ function registerRowDetailviewClick(row) {
viewContent.innerHTML = `<p class="text-danger text-center">@T["Asset not found."]</p>`; viewContent.innerHTML = `<p class="text-danger text-center">@T["Asset not found."]</p>`;
return; return;
} }
let i = 0;
const html = ` const html = `
<div class="row g-3"> <div class="row g-3">
<h4 class="fw-bold">@T["Barcode"]</h4> <h4 class="fw-bold">@T["Barcode"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<svg id="@barcodeType" class="form-control" name="Barcode" /> <svg role="img" aria-label="@T["Barcode"]" id="@barcodeType" class="form-control" name="Barcode" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<button id="downloadBtn" class="form-control my-2 btn btn-primary">@T["Download Barcode"]</button> <button id="downloadBtn" class="form-control my-2 btn btn-primary">@T["Download Barcode"]</button>
@@ -723,48 +776,48 @@ function registerRowDetailviewClick(row) {
<div class="row g-3"> <div class="row g-3">
<h4 class="fw-bold">@T["Inventory"]</h4> <h4 class="fw-bold">@T["Inventory"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Name"]</label> <label for="detailUsername" class="form-label">@T["Name"]</label>
<input type="text" class="form-control" name="Name" value="${asset.Description.Inventory.PersonUid || ''}" disabled /> <input id="detailUsername" type="text" class="form-control" name="Name" value="${asset.Description.Inventory.PersonUid || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Date"]</label> <label for="detailDate" class="form-label">@T["Date"]</label>
<input type="text" class="form-control" name="Name" value="${new Intl.DateTimeFormat('de-DE', {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date(asset.Description.Inventory.Date)) || ''}" disabled /> <input id="detailDate" type="text" class="form-control" name="Date" value="${new Intl.DateTimeFormat('de-DE', {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date(asset.Description.Inventory.Date)) || ''}" disabled />
</div> </div>
</div> </div>
<hr class="my-3" /> <hr class="my-3" />
<div class="row g-3"> <div class="row g-3">
<h4 class="fw-bold">@T["Information"]</h4> <h4 class="fw-bold">@T["Information"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Name"]</label> <label for="detailName" class="form-label">@T["Name"]</label>
<input type="text" class="form-control" name="Name" value="${asset.Name || ''}" disabled /> <input id="detailName" type="text" class="form-control" name="Name" value="${asset.Name || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Location"]</label> <label for="detailLocation" class="form-label">@T["Location"]</label>
<input type="text" class="form-control" name="Location" value="${asset.Location || ''}" disabled /> <input id="detailLocation" type="text" class="form-control" name="Location" value="${asset.Location || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Owner"]</label> <label for="detailOwner" class="form-label">@T["Owner"]</label>
<input type="text" class="form-control" name="Owner" value="${asset.Owner || ''}" disabled /> <input id="detailOwner" type="text" class="form-control" name="Owner" value="${asset.Owner || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Serial Number"]</label> <label for="detailSerialnumber" class="form-label">@T["Serial Number"]</label>
<input type="text" class="form-control" name="SerialNumber" value="${asset.SerialNumber || ''}" disabled /> <input id="detailSerialnumber" type="text" class="form-control" name="SerialNumber" value="${asset.SerialNumber || ''}" disabled />
</div> </div>
</div> </div>
<hr class="my-3" /> <hr class="my-3" />
<div class="row g-3"> <div class="row g-3">
<h4 class="fw-bold">@T["Description"]</h4> <h4 class="fw-bold">@T["Description"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Type"]</label> <label for="detailType" class="form-label">@T["Type"]</label>
<input type="text" class="form-control" name="Description.Type" value="${asset.Description?.Type || ''}" disabled /> <input id="detailType" type="text" class="form-control" name="Description.Type" value="${asset.Description?.Type || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Make"]</label> <label for="detailMake" class="form-label">@T["Make"]</label>
<input type="text" class="form-control" name="Description.Make" value="${asset.Description?.Make || ''}" disabled /> <input id="detailMake" type="text" class="form-control" name="Description.Make" value="${asset.Description?.Make || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Model"]</label> <label for="detailModel" class="form-label">@T["Model"]</label>
<input type="text" class="form-control" name="Description.Model" value="${asset.Description?.Model || ''}" disabled /> <input id="detailModel" type="text" class="form-control" name="Description.Model" value="${asset.Description?.Model || ''}" disabled />
</div> </div>
</div> </div>
${asset.Description?.Attributes ? ` ${asset.Description?.Attributes ? `
@@ -774,8 +827,8 @@ function registerRowDetailviewClick(row) {
${Object.entries(asset.Description.Attributes) ${Object.entries(asset.Description.Attributes)
.map(([k,v]) => ` .map(([k,v]) => `
<div class="d-flex gap-2 align-items-center attribute-row"> <div class="d-flex gap-2 align-items-center attribute-row">
<input type="text" class="form-control w-50" placeholder="@T["Attribute name"]" data-attr-name disabled value="${k}" />: <label for="detailAttributeName-${++i}" class="visually-hidden">${k}</label><input id="detailAttributeName-${i}" type="text" class="form-control w-50" placeholder="@T["Attribute name"]" data-attr-name disabled value="${k}" />:
<input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value disabled value="${v}" /> <label for="detailAttributeValue-${i}" class="visually-hidden">${k}</label><input id="detailAttributeValue-${i}" type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value disabled value="${v}" />
</div>`) </div>`)
.join('')} .join('')}
</div>` : ''} </div>` : ''}
@@ -785,20 +838,20 @@ function registerRowDetailviewClick(row) {
<div class="row g-3"> <div class="row g-3">
<h4 class="fw-bold">@T["Purchase Information"]</h4> <h4 class="fw-bold">@T["Purchase Information"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Purchase Date"]</label> <label for="detailPurchaseDate" class="form-label">@T["Purchase Date"]</label>
<input type="date" class="form-control" name="Description.Purchase.PurchaseDate" value="${asset.Description.Purchase.PurchaseDate || ''}" disabled /> <input id="detailPurchaseDate" type="text" class="form-control" name="Description.Purchase.PurchaseDate" value="${new Intl.DateTimeFormat('de-DE', {year: "numeric", month: "2-digit", day: "2-digit"}).format(new Date(asset.Description.Purchase.PurchaseDate)) || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Purchase Value"]</label> <label for="detailPurchaseValue" class="form-label">@T["Purchase Value"]</label>
<input type="text" class="form-control" name="Description.Purchase.PurchaseValue" value="${asset.Description.Purchase.PurchaseValue || ''}" disabled /> <input id="detailPurchaseValue" type="text" class="form-control" name="Description.Purchase.PurchaseValue" value="${asset.Description.Purchase.PurchaseValue || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Purchased At"]</label> <label for="detailPurchaseAt" class="form-label">@T["Purchased At"]</label>
<input type="text" class="form-control" name="Description.Purchase.PurchaseAt" value="${asset.Description.Purchase.PurchaseAt || ''}" disabled /> <input id="detailPurchaseAt" type="text" class="form-control" name="Description.Purchase.PurchaseAt" value="${asset.Description.Purchase.PurchaseAt || ''}" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">@T["Purchased By"]</label> <label for="detailPurchaseBy" class="form-label">@T["Purchased By"]</label>
<input type="text" class="form-control" name="Description.Purchase.PurchaseBy" value="${asset.Description.Purchase.PurchaseBy || ''}" disabled /> <input id="detailPurchaseBy" type="text" class="form-control" name="Description.Purchase.PurchaseBy" value="${asset.Description.Purchase.PurchaseBy || ''}" disabled />
</div> </div>
</div>` : ''} </div>` : ''}
</div>`; </div>`;
@@ -822,7 +875,6 @@ function registerRowDetailviewClick(row) {
console.error(err); console.error(err);
viewContent.innerHTML = `<p class="text-danger text-center">@T["Error loading asset details"]</p>`; viewContent.innerHTML = `<p class="text-danger text-center">@T["Error loading asset details"]</p>`;
} }
});
} }
@@ -876,12 +928,18 @@ document.addEventListener('DOMContentLoaded', () => {
if (anyEquals) { if (anyEquals) {
anyEqualsElement.value = attributeValue; anyEqualsElement.value = attributeValue;
} else { } else {
const row = document.createElement('div'); const row = document.createElement('tr');
row.className = 'd-flex gap-2 align-items-center attribute-row'; row.className = 'attribute-row mb-3';
row.innerHTML = ` row.innerHTML = `
<td class="col-md-5 p-1">
<input type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name value="${attributeKey}" /> <input type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name value="${attributeKey}" />
</td>
<td class="col-md-5 p-1">
<input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value value="${attributeValue}" /> <input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value value="${attributeValue}" />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button> <button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`; `;
attributesContainer.appendChild(row); attributesContainer.appendChild(row);
} }

View File

@@ -39,12 +39,12 @@
{ {
<tr> <tr>
<td class="text-center">@groupTableViewModel.Group</td> <td class="text-center">@groupTableViewModel.Group</td>
<td class="text-center @(groupTableViewModel.CanInventorize ? "text-success" : "text-danger")">@(groupTableViewModel.CanInventorize ? "✓" : "✗")</td> <td class="text-center @(groupTableViewModel.CanInventorize ? "text-success" : "text-danger")"><span class="visually-hidden">@T["inventorize"]</span>@(groupTableViewModel.CanInventorize ? Html.Raw($"<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">{@T["Yes"].Value}</span>") : Html.Raw($"<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">{@T["No"].Value}</span>"))</td>
<td class="text-center @(groupTableViewModel.CanManageUsers ? "text-success" : "text-danger")">@(groupTableViewModel.CanManageUsers ? "✓" : "✗")</td> <td class="text-center @(groupTableViewModel.CanManageUsers ? "text-success" : "text-danger")"><span class="visually-hidden">@T["manage users"]</span>@(groupTableViewModel.CanManageUsers ? Html.Raw($"<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">{@T["Yes"].Value}</span>") : Html.Raw($"<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">{@T["No"].Value}</span>"))</td>
<td class="text-center @(groupTableViewModel.CanManageLocations ? "text-success" : "text-danger")">@(groupTableViewModel.CanManageLocations ? "✓" : "✗")</td> <td class="text-center @(groupTableViewModel.CanManageLocations ? "text-success" : "text-danger")"><span class="visually-hidden">@T["manage locations"]</span>@(groupTableViewModel.CanManageLocations ? Html.Raw($"<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">{@T["Yes"].Value}</span>") : Html.Raw($"<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">{@T["No"].Value}</span>"))</td>
<td class="text-center @(groupTableViewModel.CanManageAssets ? "text-success" : "text-danger")">@(groupTableViewModel.CanManageAssets ? "✓" : "✗")</td> <td class="text-center @(groupTableViewModel.CanManageAssets ? "text-success" : "text-danger")"><span class="visually-hidden">@T["manage assets"]</span>@(groupTableViewModel.CanManageAssets ? Html.Raw($"<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">{@T["Yes"].Value}</span>") : Html.Raw($"<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">{@T["No"].Value}</span>"))</td>
<td class="text-center @(groupTableViewModel.CanManageGroups ? "text-success" : "text-danger")">@(groupTableViewModel.CanManageGroups ? "✓" : "✗")</td> <td class="text-center @(groupTableViewModel.CanManageGroups ? "text-success" : "text-danger")"><span class="visually-hidden">@T["manage groups"]</span>@(groupTableViewModel.CanManageGroups ? Html.Raw($"<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">{@T["Yes"].Value}</span>") : Html.Raw($"<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">{@T["No"].Value}</span>"))</td>
<td class="text-center @(groupTableViewModel.CanManageSettings ? "text-success" : "text-danger")">@(groupTableViewModel.CanManageSettings ? "✓" : "✗")</td> <td class="text-center @(groupTableViewModel.CanManageSettings ? "text-success" : "text-danger")"><span class="visually-hidden">@T["manage settings"]</span>@(groupTableViewModel.CanManageSettings ? Html.Raw($"<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">{@T["Yes"].Value}</span>") : Html.Raw($"<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">{@T["No"].Value}</span>"))</td>
<td class="text-center"> <td class="text-center">
<div class="d-flex gap-2 justify-content-center"> <div class="d-flex gap-2 justify-content-center">
<button class="btn btn-sm btn-warning btn-update" <button class="btn btn-sm btn-warning btn-update"
@@ -78,7 +78,7 @@
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>@T["GroupDeleteConfirmation1"] <strong id="groupName"></strong> (ID: <span id="groupId"></span>)@T["GroupDeleteConfirmation2"]</p> <p>@T["GroupDeleteConfirmation1"] <strong id="deleteGroupName"></strong> (ID: <span id="deleteGroupId"></span>)@T["GroupDeleteConfirmation2"]</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@T["Cancel"]</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@T["Cancel"]</button>
@@ -101,8 +101,8 @@
const groupId = currentButton.getAttribute('data-group-id'); const groupId = currentButton.getAttribute('data-group-id');
const groupName = currentButton.getAttribute('data-group-name'); const groupName = currentButton.getAttribute('data-group-name');
deleteModal.querySelector('#groupId').textContent = groupId; deleteModal.querySelector('#deleteGroupId').textContent = groupId;
deleteModal.querySelector('#groupName').textContent = groupName; deleteModal.querySelector('#deleteGroupName').textContent = groupName;
// Store the delete URL for later use // Store the delete URL for later use
deleteModal.querySelector('#deleteForm').dataset.url = `/Groups/Delete?uid=${groupId}`; deleteModal.querySelector('#deleteForm').dataset.url = `/Groups/Delete?uid=${groupId}`;
@@ -114,7 +114,7 @@
e.preventDefault(); e.preventDefault();
const url = deleteForm.dataset.url; const url = deleteForm.dataset.url;
const groupId = deleteModal.querySelector('#groupId').textContent; const groupId = deleteModal.querySelector('#deleteGroupId').textContent;
try { try {
const response = await fetch(url, { const response = await fetch(url, {
@@ -171,16 +171,16 @@
<div class="row g-3 justify-content-center"> <div class="row g-3 justify-content-center">
<!-- Basic Info --> <!-- Basic Info -->
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="cn">@T["Group ID"] *</label> <label class="form-label" for="cn">@T["Group ID"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="cn" class="form-control" name="Cn" required /> <input type="text" id="cn" class="form-control" name="Cn" aria-required="true" required />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="displayname">@T["Display Name"] *</label> <label class="form-label" for="displayname">@T["Display Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="displayname" class="form-control" name="DisplayName" /> <input type="text" id="displayname" class="form-control" name="DisplayName" aria-required="true" required />
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="Permissions.CanInventorize" id="canInventorize" /> <input type="checkbox" class="form-check-input" name="Permissions.CanInventorize" id="canInventorize" />
<label class="form-check-label" for="canInventorize">@T["Can inventorize"]</label> <label class="form-check-label" for="canInventorize">@T["Can inventorize"]</label>
</div> </div>
@@ -190,7 +190,7 @@
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="Permissions.CanManageUsers" id="canManageUsers" /> <input type="checkbox" class="form-check-input" name="Permissions.CanManageUsers" id="canManageUsers" />
<label class="form-check-label" for="canManageUsers">@T["Can manage users"]</label> <label class="form-check-label" for="canManageUsers">@T["Can manage users"]</label>
</div> </div>
@@ -200,7 +200,7 @@
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="Permissions.CanManageLocations" id="canManageLocations" /> <input type="checkbox" class="form-check-input" name="Permissions.CanManageLocations" id="canManageLocations" />
<label class="form-check-label" for="canManageLocations">@T["Can manage locations"]</label> <label class="form-check-label" for="canManageLocations">@T["Can manage locations"]</label>
</div> </div>
@@ -223,6 +223,9 @@
<script> <script>
const contentYes = `<span aria-hidden=\"true\">✓</span><span class=\"visually-hidden\">@T["Yes"].Value</span>`;
const contentNo = `<span aria-hidden=\"true\">✗</span><span class=\"visually-hidden\">@T["No"].Value</span>`;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const createForm = document.getElementById('createGroupForm'); const createForm = document.getElementById('createGroupForm');
@@ -264,16 +267,15 @@
// Add the new group to the table // Add the new group to the table
const tableBody = document.querySelector('tbody'); const tableBody = document.querySelector('tbody');
const newRow = document.createElement('tr'); const newRow = document.createElement('tr');
newRow.innerHTML = ` newRow.innerHTML = `
<td style="text-align: center">${jsonData.DisplayName}</td> <td class="text-center">${jsonData.DisplayName}</td>
<td class="text-center ${jsonData.Permissions.includes("CanInventorize") ? "text-success" : "text-danger"}">${jsonData.Permissions.includes("CanInventorize") ? "✓" : "✗"}</td> <td class="text-center ${jsonData.Permissions.includes("CanInventorize") ? "text-success" : "text-danger"}"><span class="visually-hidden">@T["inventorize"]</span>${jsonData.Permissions.includes("CanInventorize") ? contentYes : contentNo}</td>
<td class="text-center ${jsonData.Permissions.includes("CanManageUsers") ? "text-success" : "text-danger"}">${jsonData.Permissions.includes("CanManageUsers") ? "✓" : "✗"}</td> <td class="text-center ${jsonData.Permissions.includes("CanManageUsers") ? "text-success" : "text-danger"}"><span class="visually-hidden">@T["manage users"]</span>${jsonData.Permissions.includes("CanManageUsers") ? contentYes : contentNo}</td>
<td class="text-center ${jsonData.Permissions.includes("CanManageLocations") ? "text-success" : "text-danger"}">${jsonData.Permissions.includes("CanManageLocations") ? "✓" : "✗"}</td> <td class="text-center ${jsonData.Permissions.includes("CanManageLocations") ? "text-success" : "text-danger"}"><span class="visually-hidden">@T["manage locations"]</span>${jsonData.Permissions.includes("CanManageLocations") ? contentYes : contentNo}</td>
<td class="text-center ${jsonData.Permissions.includes("CanManageAssets") ? "text-success" : "text-danger"}">${jsonData.Permissions.includes("CanManageAssets") ? "✓" : "✗"}</td> <td class="text-center ${jsonData.Permissions.includes("CanManageAssets") ? "text-success" : "text-danger"}"><span class="visually-hidden">@T["manage assets"]</span>${jsonData.Permissions.includes("CanManageAssets") ? contentYes : contentNo}</td>
<td class="text-center ${jsonData.Permissions.includes("CanManageGroups") ? "text-success" : "text-danger"}">${jsonData.Permissions.includes("CanManageGroups") ? "✓" : "✗"}</td> <td class="text-center ${jsonData.Permissions.includes("CanManageGroups") ? "text-success" : "text-danger"}"><span class="visually-hidden">@T["manage groups"]</span>${jsonData.Permissions.includes("CanManageGroups") ? contentYes : contentNo}</td>
<td class="text-center ${jsonData.Permissions.includes("CanManageSettings") ? "text-success" : "text-danger"}">${jsonData.Permissions.includes("CanManageSettings") ? "✓" : "✗"}</td> <td class="text-center ${jsonData.Permissions.includes("CanManageSettings") ? "text-success" : "text-danger"}"><span class="visually-hidden">@T["manage settings"]</span>${jsonData.Permissions.includes("CanManageSettings") ? contentYes : contentNo}</td>
<td style="text-align: center"> <td class="text-center">
<div class="d-flex gap-2 justify-content-center"> <div class="d-flex gap-2 justify-content-center">
<button class="btn btn-sm btn-warning btn-update" <button class="btn btn-sm btn-warning btn-update"
data-group-id="${jsonData.Cn}" data-group-id="${jsonData.Cn}"
@@ -325,45 +327,45 @@
<form id="updateGroupForm"> <form id="updateGroupForm">
<div class="modal-body"> <div class="modal-body">
<div class="row g-3 justify-content-center"> <div class="row g-3 mb-2 justify-content-center">
<!-- Basic Info --> <!-- Basic Info -->
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="groupId">@T["Group ID"] *</label> <label class="form-label" for="groupId">@T["Group ID"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="groupId" class="form-control" name="Cn" required /> <input type="text" id="groupId" class="form-control" name="Cn" aria-required="true" required />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="dn">@T["Display Name"] *</label> <label class="form-label" for="dn">@T["Display Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="dn" class="form-control" name="DisplayName" /> <input type="text" id="dn" class="form-control" name="DisplayName" aria-required="true" required />
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="Description.Permissions.CanInventorize" id="canInventorize" /> <input type="checkbox" class="form-check-input" name="Description.Permissions.CanInventorize" id="updateCanInventorize" />
<label class="form-check-label" for="canInventorize">@T["Can inventorize"]</label> <label class="form-check-label" for="updateCanInventorize">@T["Can inventorize"]</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageAssets" id="canManageAssets" /> <input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageAssets" id="updateCanManageAssets" />
<label class="form-check-label" for="canManageAssets">@T["Can manage assets"]</label> <label class="form-check-label" for="updateCanManageAssets">@T["Can manage assets"]</label>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageUsers" id="canManageUsers" /> <input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageUsers" id="updateCanManageUsers" />
<label class="form-check-label" for="canManageUsers">@T["Can manage users"]</label> <label class="form-check-label" for="updateCanManageUsers">@T["Can manage users"]</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageGroups" id="canManageGroups" /> <input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageGroups" id="updateCanManageGroups" />
<label class="form-check-label" for="canManageGroups">@T["Can manage groups"]</label> <label class="form-check-label" for="updateCanManageGroups">@T["Can manage groups"]</label>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageLocations" id="canManageLocations" /> <input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageLocations" id="updateCanManageLocations" />
<label class="form-check-label" for="canManageLocations">@T["Can manage locations"]</label> <label class="form-check-label" for="updateCanManageLocations">@T["Can manage locations"]</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageSettings" id="canManageSettings" /> <input type="checkbox" class="form-check-input" name="Description.Permissions.CanManageSettings" id="updateCanManageSettings" />
<label class="form-check-label" for="canManageSettings">@T["Can manage settings"]</label> <label class="form-check-label" for="updateCanManageSettings">@T["Can manage settings"]</label>
</div> </div>
</div> </div>
</div> </div>
@@ -462,12 +464,12 @@ document.addEventListener('DOMContentLoaded', () => {
.find(r => r.querySelector(`[data-group-id="${jsonData.Cn}"]`)); .find(r => r.querySelector(`[data-group-id="${jsonData.Cn}"]`));
if (row) { if (row) {
row.children[0].textContent = jsonData.Description.DisplayName || ''; row.children[0].textContent = jsonData.Description.DisplayName || '';
row.children[1].textContent = jsonData.Description.Permissions.includes("CanInventorize") ? "✓" : "✗" || ''; row.children[1].innerHTML = `<span class="visually-hidden">@T["inventorize"]</span>` + (jsonData.Description.Permissions.includes("CanInventorize") ? contentYes : contentNo || '');
row.children[2].textContent = jsonData.Description.Permissions.includes("CanManageUsers") ? "✓" : "✗" || ''; row.children[2].innerHTML = `<span class="visually-hidden">@T["manage users"]</span>` + (jsonData.Description.Permissions.includes("CanManageUsers") ? contentYes : contentNo || '');
row.children[3].textContent = jsonData.Description.Permissions.includes("CanManageLocations") ? "✓" : "✗" || ''; row.children[3].innerHTML = `<span class="visually-hidden">@T["manage locations"]</span>` + (jsonData.Description.Permissions.includes("CanManageLocations") ? contentYes : contentNo || '');
row.children[4].textContent = jsonData.Description.Permissions.includes("CanManageAssets") ? "✓" : "✗" || ''; row.children[4].innerHTML = `<span class="visually-hidden">@T["manage assets"]</span>` + (jsonData.Description.Permissions.includes("CanManageAssets") ? contentYes : contentNo || '');
row.children[5].textContent = jsonData.Description.Permissions.includes("CanManageGroups") ? "✓" : "✗" || ''; row.children[5].innerHTML = `<span class="visually-hidden">@T["manage groups"]</span>` + (jsonData.Description.Permissions.includes("CanManageGroups") ? contentYes : contentNo || '');
row.children[6].textContent = jsonData.Description.Permissions.includes("CanManageSettings") ? "✓" : "✗" || ''; row.children[6].innerHTML = `<span class="visually-hidden">@T["manage settings"]</span>` + (jsonData.Description.Permissions.includes("CanManageSettings") ? contentYes : contentNo || '');
if (jsonData.Description.Permissions.includes("CanInventorize")) { if (jsonData.Description.Permissions.includes("CanInventorize")) {
row.children[1].className = "text-center text-success"; row.children[1].className = "text-center text-success";
} else { } else {

View File

@@ -52,4 +52,13 @@
} }
</div> </div>
</div> </div>
<div class="row text-center">
<div class="mb-4 d-flex flex-wrap gap-2 justify-content-center">
<a asp-controller="Settings" asp-action="User">@T["User settings"]</a>
@if (User.HasClaim(ClaimTypes.Role, "CanManageSettings"))
{
<a asp-controller="Settings" asp-action="Admin">@T["Admin settings"]</a>
}
</div>
</div>
</div> </div>

View File

@@ -17,6 +17,7 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-3 text-center"> <div class="col-md-3 text-center">
<label for="barcodeInput" class="visually-hidden">@T["Asset ID"]</label>
<input type="text" id="barcodeInput" class="form-control mt-3" placeholder="@T["Asset ID"]" /> <input type="text" id="barcodeInput" class="form-control mt-3" placeholder="@T["Asset ID"]" />
<button id="enterAssetIdManuallyButton" class="btn btn-secondary mt-3">@T["Enter asset ID manually"]</button> <button id="enterAssetIdManuallyButton" class="btn btn-secondary mt-3">@T["Enter asset ID manually"]</button>
<div id="reader" style="display:none" class="mt-3"></div> <div id="reader" style="display:none" class="mt-3"></div>
@@ -72,9 +73,11 @@
<script defer> <script defer>
async function onScanSuccess(decodedText, decodedResult) { async function onScanSuccess(decodedText, decodedResult, isEnteredManually) {
const rawDecoded = decodedText; const rawDecoded = decodedText;
const BARCODE_TYPE = "@barcodeType"; const BARCODE_TYPE = "@barcodeType";
if (!isEnteredManually)
{
switch (BARCODE_TYPE.toUpperCase()) { switch (BARCODE_TYPE.toUpperCase()) {
case "EAN13": case "EAN13":
decodedText = decodedText.slice(0,-1); decodedText = decodedText.slice(0,-1);
@@ -101,6 +104,7 @@
decodedText = decodedText.slice(0,-1); decodedText = decodedText.slice(0,-1);
break; break;
} }
}
decodedText = decodedText.replace(/^0+/, ''); decodedText = decodedText.replace(/^0+/, '');
console.log(`Code matched = ${decodedText}`, decodedResult); console.log(`Code matched = ${decodedText}`, decodedResult);
document.getElementById("barcodeInput").value = decodedText; document.getElementById("barcodeInput").value = decodedText;
@@ -144,19 +148,19 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailName">@T["Name"]</label> <label class="form-label" for="detailName">@T["Name"]</label>
<input type="text" class="form-control" id="detailName" name="Name" value="${asset.Name || ''}" disabled /> <input type="text" class="form-control" id="detailName" name="Name" value="${asset.Name || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailLocation">@T["Location"]</label> <label class="form-label" for="detailLocation">@T["Location"]</label>
<input type="text" class="form-control" id="detailLocation" name="Location" value="${asset.Location || ''}" disabled /> <input type="text" class="form-control" id="detailLocation" name="Location" value="${asset.Location || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailOwner">@T["Owner"]</label> <label class="form-label" for="detailOwner">@T["Owner"]</label>
<input type="text" class="form-control" id="detailOwner" name="Owner" value="${asset.Owner || ''}" disabled /> <input type="text" class="form-control" id="detailOwner" name="Owner" value="${asset.Owner || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailSerialNumber">@T["Serial Number"]</label> <label class="form-label" for="detailSerialNumber">@T["Serial Number"]</label>
<input type="text" class="form-control" id="detailSerialNumber" name="SerialNumber" value="${asset.SerialNumber || ''}" disabled /> <input type="text" class="form-control" id="detailSerialNumber" name="SerialNumber" value="${asset.SerialNumber || ''}" readonly />
</div> </div>
</div> </div>
<hr class="my-3" /> <hr class="my-3" />
@@ -164,28 +168,44 @@
<h4 class="fw-bold">@T["Description"]</h4> <h4 class="fw-bold">@T["Description"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailType">@T["Type"]</label> <label class="form-label" for="detailType">@T["Type"]</label>
<input type="text" class="form-control" id="detailType" name="Description.Type" value="${asset.Description?.Type || ''}" disabled /> <input type="text" class="form-control" id="detailType" name="Description.Type" value="${asset.Description?.Type || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailMake">@T["Make"]</label> <label class="form-label" for="detailMake">@T["Make"]</label>
<input type="text" class="form-control" id="detailMake" name="Description.Make" value="${asset.Description?.Make || ''}" disabled /> <input type="text" class="form-control" id="detailMake" name="Description.Make" value="${asset.Description?.Make || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailModel">@T["Model"]</label> <label class="form-label" for="detailModel">@T["Model"]</label>
<input type="text" class="form-control" id="detailModel" name="Description.Model" value="${asset.Description?.Model || ''}" disabled /> <input type="text" class="form-control" id="detailModel" name="Description.Model" value="${asset.Description?.Model || ''}" readonly />
</div> </div>
</div> </div>
${asset.Description?.Attributes ? ` ${asset.Description?.Attributes ? `
<hr class="my-3" /> <hr class="my-3" />
<div class="row g-3"> <div class="row g-3">
<h4 class="fw-bold">@T["Attributes"]</h4> <h4 class="fw-bold">@T["Attributes"]</h4>
<table class="w-100">
<thead>
<tr>
<th class="visually-hidden">@T["Attribute name"]</th>
<th class="visually-hidden">@T["Attribute value"]</th>
<th class="visually-hidden">@T["Delete"]</th>
</tr>
</thead>
<tbody id="updateAttributesContainer">
${Object.entries(asset.Description.Attributes) ${Object.entries(asset.Description.Attributes)
.map(([k,v]) => ` .map(([k,v]) => `
<div class="d-flex gap-2 align-items-center attribute-row"> <tr class="gap-2 align-items-center attribute-row">
<input type="text" class="form-control w-50" placeholder="@T["Attribute name"]" aria-label="@T["Attribute name"]" data-attr-name disabled value="${k}" />: <td class="d-flex p-1 align-items-center">
<input type="text" class="form-control" placeholder="@T["Attribute value"]" aria-label="@T["Attribute value"]" data-attr-value disabled value="${v}" /> <input type="text" class="form-control d-inline-block w-100" placeholder="@T["Attribute name"]" aria-label="@T["Attribute name"]" data-attr-name readonly value="${k}" />
</div>`) <span class="ms-1">:</span>
</td>
<td class="p-1">
<input type="text" class="form-control" placeholder="@T["Attribute value"]" aria-label="@T["Attribute value"]" data-attr-value readonly value="${v}" />
</td>
</tr>`)
.join('')} .join('')}
</tbody>
</table>
</div>` : ''} </div>` : ''}
${asset.Description?.Purchase ? ` ${asset.Description?.Purchase ? `
@@ -194,19 +214,19 @@
<h4 class="fw-bold">@T["Purchase Information"]</h4> <h4 class="fw-bold">@T["Purchase Information"]</h4>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailPurchaseDate">@T["Purchase Date"]</label> <label class="form-label" for="detailPurchaseDate">@T["Purchase Date"]</label>
<input type="date" class="form-control" id="detailPurchaseDate" name="Description.Purchase.PurchaseDate" value="${asset.Description.Purchase.PurchaseDate || ''}" disabled /> <input type="text" class="form-control" id="detailPurchaseDate" name="Description.Purchase.PurchaseDate" value="${asset.Description.Purchase.PurchaseDate || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailPurchaseValue">@T["Purchase Value"]</label> <label class="form-label" for="detailPurchaseValue">@T["Purchase Value"]</label>
<input type="text" class="form-control" id="detailPurchaseValue" name="Description.Purchase.PurchaseValue" value="${asset.Description.Purchase.PurchaseValue || ''}" disabled /> <input type="text" class="form-control" id="detailPurchaseValue" name="Description.Purchase.PurchaseValue" value="${asset.Description.Purchase.PurchaseValue || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailPurchaseAt">@T["Purchased At"]</label> <label class="form-label" for="detailPurchaseAt">@T["Purchased At"]</label>
<input type="text" class="form-control" id="detailPurchaseAt" name="Description.Purchase.PurchaseAt" value="${asset.Description.Purchase.PurchaseAt || ''}" disabled /> <input type="text" class="form-control" id="detailPurchaseAt" name="Description.Purchase.PurchaseAt" value="${asset.Description.Purchase.PurchaseAt || ''}" readonly />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="detailPurchaseBy">@T["Purchased By"]</label> <label class="form-label" for="detailPurchaseBy">@T["Purchased By"]</label>
<input type="text" class="form-control" id="detailPurchaseBy" name="Description.Purchase.PurchaseBy" value="${asset.Description.Purchase.PurchaseBy || ''}" disabled /> <input type="text" class="form-control" id="detailPurchaseBy" name="Description.Purchase.PurchaseBy" value="${asset.Description.Purchase.PurchaseBy || ''}" readonly />
</div> </div>
</div>` : ''} </div>` : ''}
</div>`; </div>`;
@@ -251,7 +271,7 @@
console.warn("Could not stop scanner:", err); console.warn("Could not stop scanner:", err);
} }
} }
await onScanSuccess(document.getElementById("barcodeInput").value, null); await onScanSuccess(document.getElementById("barcodeInput").value, null, true);
}); });
} }
const scanBarcodeButton = document.querySelector('#scanBarcodeButton'); const scanBarcodeButton = document.querySelector('#scanBarcodeButton');
@@ -385,10 +405,21 @@
</div> </div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="justify-content-between align-items-center mb-2">
<h4 class="fw-bold mb-0">@T["Attributes"]</h4> <h4 class="fw-bold mb-0">@T["Attributes"]</h4>
<a href="#updateAddAttributeBtn" class="visually-hidden-focusable">@T["Jump to add attribute button"]</a>
<table class="w-100">
<thead>
<tr>
<th class="visually-hidden">@T["Attribute name"]</th>
<th class="visually-hidden">@T["Attribute value"]</th>
<th class="visually-hidden">@T["Delete"]</th>
</tr>
</thead>
<tbody id="updateAttributesContainer">
</tbody>
</table>
</div> </div>
<div id="updateAttributesContainer" class="d-flex flex-column gap-2"></div>
<button type="button" class="btn btn-sm btn-primary mt-3" id="updateAddAttributeBtn"> <button type="button" class="btn btn-sm btn-primary mt-3" id="updateAddAttributeBtn">
@T["Add Attribute"] @T["Add Attribute"]
</button> </button>
@@ -435,12 +466,18 @@ document.addEventListener('DOMContentLoaded', () => {
let assetId = null; let assetId = null;
addAttrBtn.addEventListener('click', () => { addAttrBtn.addEventListener('click', () => {
const row = document.createElement('div'); const row = document.createElement('tr');
row.className = 'd-flex gap-2 align-items-center attribute-row'; row.className = 'attribute-row mb-3';
row.innerHTML = ` row.innerHTML = `
<input type="text" class="form-control" aria-label="@T["Attribute name"]" placeholder="@T["Attribute name"]" data-attr-name /> <td class="col-md-5 p-1">
<input type="text" class="form-control" aria-label="@T["Attribute value"]" placeholder="@T["Attribute value"]" data-attr-value /> <input type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name />
</td>
<td class="col-md-5 p-1">
<input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button> <button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`; `;
updateAttributesContainer.appendChild(row); updateAttributesContainer.appendChild(row);
}); });
@@ -486,12 +523,18 @@ document.addEventListener('DOMContentLoaded', () => {
// Attributes // Attributes
if (asset.Description.Attributes) { if (asset.Description.Attributes) {
for (const [attrName, attrValue] of Object.entries(asset.Description.Attributes)) { for (const [attrName, attrValue] of Object.entries(asset.Description.Attributes)) {
const row = document.createElement('div'); const row = document.createElement('tr');
row.className = 'd-flex gap-2 align-items-center attribute-row'; row.className = 'attribute-row mb-3';
row.innerHTML = ` row.innerHTML = `
<input type="text" class="form-control" aria-label="@T["Attribute name"]" value="${attrName}" data-attr-name /> <td class="col-md-5 p-1">
<input type="text" class="form-control" aria-label="@T["Attribute value"]" value="${attrValue}" data-attr-value /> <input type="text" class="form-control" value="${attrName}" placeholder="@T["Attribute name"]" data-attr-name />
</td>
<td class="col-md-5 p-1">
<input type="text" class="form-control" value="${attrValue}" placeholder="@T["Attribute value"]" data-attr-value />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button> <button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`; `;
updateAttributesContainer.appendChild(row); updateAttributesContainer.appendChild(row);
} }

View File

@@ -165,8 +165,8 @@
<input type="hidden" id="editLocationId" name="LocationID"> <input type="hidden" id="editLocationId" name="LocationID">
<div class="mb-3"> <div class="mb-3">
<label for="editLocationName" class="form-label">@T["Location Name"]</label> <label for="editLocationName" class="form-label">@T["Location Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" class="form-control" id="editLocationName" name="LocationName" required> <input type="text" class="form-control" id="editLocationName" name="LocationName" aria-required="true" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -234,6 +234,7 @@
const result = await response.json(); const result = await response.json();
if (result) { if (result) {
const btn = document.querySelector(`button[data-location-id="${data.Location}"]`); const btn = document.querySelector(`button[data-location-id="${data.Location}"]`);
const btn_del = document.querySelector(`button.btn-danger[data-location-id="${data.Location}"]`);
const row = btn.closest('tr'); const row = btn.closest('tr');
let slugifiedLocationID = `${data.Description.Location}-${data.Description.RoomNumber}-${data.Description.Seat}` let slugifiedLocationID = `${data.Description.Location}-${data.Description.RoomNumber}-${data.Description.Seat}`
.toLowerCase() .toLowerCase()
@@ -244,6 +245,10 @@
btn.setAttribute("data-location-name", data.Description.Location); btn.setAttribute("data-location-name", data.Description.Location);
btn.setAttribute("data-room-number", data.Description.RoomNumber); btn.setAttribute("data-room-number", data.Description.RoomNumber);
btn.setAttribute("data-seat", data.Description.Seat); btn.setAttribute("data-seat", data.Description.Seat);
btn_del.setAttribute("data-location-id", slugifiedLocationID);
btn_del.setAttribute("data-location-name", data.Description.Location);
btn_del.setAttribute("data-room-number", data.Description.RoomNumber);
btn_del.setAttribute("data-seat", data.Description.Seat);
row.children[0].textContent = slugifiedLocationID; row.children[0].textContent = slugifiedLocationID;
row.children[1].textContent = data.Description.Location; row.children[1].textContent = data.Description.Location;
row.children[2].textContent = data.Description.RoomNumber; row.children[2].textContent = data.Description.RoomNumber;
@@ -276,8 +281,8 @@
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="createLocationName" class="form-label">@T["Location Name"]</label> <label for="createLocationName" class="form-label">@T["Location Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" class="form-control" id="createLocationName" name="LocationName" required> <input type="text" class="form-control" id="createLocationName" name="LocationName" aria-required="true" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@@ -15,17 +15,5 @@
</div> </div>
} }
} }
<form method="post" action="/Home/Login" class="mt-4" style="max-width: 400px; margin: auto;"> <partial name="_Login" />
<div class="form-group mb-3">
<label for="username" class="form-label">@T["Username"]</label>
<input autofocus type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group mb-3">
<label for="password" class="form-label">@T["Password"]</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">@T["Login"]</button>
</form>
</div> </div>

View File

@@ -176,12 +176,12 @@
<input type="text" id="updateTitle" name="Title" class="form-control" /> <input type="text" id="updateTitle" name="Title" class="form-control" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="updateName">@T["Name"]</label> <label class="form-label" for="updateName">@T["Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="updateName" name="Cn" class="form-control" /> <input type="text" id="updateName" name="Cn" aria-required="true" required class="form-control" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="updateSurname">@T["Surname"]</label> <label class="form-label" for="updateSurname">@T["Surname"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="updateSurname" name="Sn" class="form-control" /> <input type="text" id="updateSurname" name="Sn" aria-required="true" required class="form-control" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="updateBirthdate">@T["Birth Date"]</label> <label class="form-label" for="updateBirthdate">@T["Birth Date"]</label>
@@ -197,7 +197,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="updateAddressStreetNr">@T["Street Nr."]</label> <label class="form-label" for="updateAddressStreetNr">@T["Street Nr."]</label>
<input type="text" id="updateAddressStreetNr" name="Description.Address.StreetNr" class="form-control" /> <input type="number" id="updateAddressStreetNr" name="Description.Address.StreetNr" class="form-control" />
</div> </div>
<hr class="my-3"> <hr class="my-3">
<h4 class="fw-bold">@T["Workplace & account"]</h4> <h4 class="fw-bold">@T["Workplace & account"]</h4>
@@ -213,7 +213,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="updatePassword">@T["New Password"]</label> <label class="form-label" for="updatePassword">@T["New Password"]</label>
<input type="password" id="updatePassword" name="UserPassword" class="form-control" /> <input type="password" id="updatePassword" name="UserPassword" class="form-control" pattern="(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}" title="@T["Password must be at least 8 characters long and include upper, lower, number, and special character"]" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="updatePhotoFile">@T["Photo"]</label> <label class="form-label" for="updatePhotoFile">@T["Photo"]</label>
@@ -278,6 +278,9 @@
var dataFromEntries = Object.fromEntries(new FormData(updateForm).entries()); var dataFromEntries = Object.fromEntries(new FormData(updateForm).entries());
var data = unflatten(dataFromEntries); var data = unflatten(dataFromEntries);
data.Description.Groups = Array.from(updateForm.querySelector('#updateGroups').selectedOptions).map(option => option.value); data.Description.Groups = Array.from(updateForm.querySelector('#updateGroups').selectedOptions).map(option => option.value);
if (data.Description.Address.StreetNr == "") {
delete(data.Description.Address.StreetNr);
}
try { try {
const response = await fetch('/Users/Update', { const response = await fetch('/Users/Update', {
method: 'POST', method: 'POST',
@@ -434,12 +437,12 @@
<input type="text" name="Title" id="createTitle" class="form-control" /> <input type="text" name="Title" id="createTitle" class="form-control" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="createName">@T["Name"]</label> <label class="form-label" for="createName">@T["Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" name="Cn" id="createName" class="form-control" /> <input type="text" name="Cn" id="createName" aria-required="true" required class="form-control" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="createSurname">@T["Surname"]</label> <label class="form-label" for="createSurname">@T["Surname"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" name="Sn" id="createSurname" class="form-control" /> <input type="text" name="Sn" id="createSurname" aria-required="true" required class="form-control" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="createBirthDate">@T["Birth Date"]</label> <label class="form-label" for="createBirthDate">@T["Birth Date"]</label>
@@ -455,7 +458,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="createStreetNr">@T["Street Nr."]</label> <label class="form-label" for="createStreetNr">@T["Street Nr."]</label>
<input type="text" id="createStreetNr" name="Description.Address.StreetNr" class="form-control" /> <input type="number" id="createStreetNr" name="Description.Address.StreetNr" class="form-control" />
</div> </div>
<hr class="my-3"> <hr class="my-3">
<h4 class="fw-bold">@T["Workplace & account"]</h4> <h4 class="fw-bold">@T["Workplace & account"]</h4>
@@ -470,8 +473,8 @@
<select id="createGroups" id="createGroups" name="Description.Groups" class="form-select" multiple></select> <select id="createGroups" id="createGroups" name="Description.Groups" class="form-select" multiple></select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="createPassword">@T["Password"]</label> <label class="form-label" for="createPassword">@T["Password"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="password" id="createPassword" name="UserPassword" class="form-control" /> <input type="password" id="createPassword" name="UserPassword" class="form-control" pattern="(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}" title="@T["Password must be at least 8 characters long and include upper, lower, number, and special character"]" aria-required="true" required />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label" for="createPhotoFile">@T["Photo"]</label> <label class="form-label" for="createPhotoFile">@T["Photo"]</label>
@@ -539,7 +542,9 @@
const dataFromEntries = Object.fromEntries(new FormData(createForm).entries()); const dataFromEntries = Object.fromEntries(new FormData(createForm).entries());
const data = unflatten(dataFromEntries); const data = unflatten(dataFromEntries);
data.Description.Groups = Array.from(createGroupsSelect.selectedOptions).map(o => o.value); data.Description.Groups = Array.from(createGroupsSelect.selectedOptions).map(o => o.value);
if (data.Description.Address.StreetNr == "") {
delete(data.Description.Address.StreetNr);
}
try { try {
const response = await fetch('/Users/Create', { const response = await fetch('/Users/Create', {
method: 'POST', method: 'POST',
@@ -572,7 +577,7 @@
data-user-birthdate="${data.Description.BirthDate}" data-user-birthdate="${data.Description.BirthDate}"
data-user-address-city="${data.Description.Address.City}" data-user-address-city="${data.Description.Address.City}"
data-user-address-street="${data.Description.Address.Street}" data-user-address-street="${data.Description.Address.Street}"
data-user-address-streetnr="${data.Description.Address.StreetNr}" data-user-address-streetnr="${data.Description.Address.StreetNr || ""}"
data-user-workplace="${data.Description?.Workplace || ''}" data-user-workplace="${data.Description?.Workplace || ''}"
data-user-groups='${JSON.stringify(data.Description?.Groups || [])}' data-user-groups='${JSON.stringify(data.Description?.Groups || [])}'
data-bs-toggle="modal" data-bs-toggle="modal"
@@ -580,7 +585,7 @@
@T["Update"] @T["Update"]
</button> </button>
<button class="btn btn-sm btn-danger btn-delete" <button class="btn btn-sm btn-danger btn-delete"
data-user-id="${result.NewUid || ''}" data-user-id="${result.Uid || ''}"
data-user-name="${data.Cn || ''}" data-user-name="${data.Cn || ''}"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#deleteModal"> data-bs-target="#deleteModal">
@@ -661,11 +666,11 @@
<label class="form-label" for="detailCity">@T["City"]</label> <label class="form-label" for="detailCity">@T["City"]</label>
<input type="text" class="form-control" id="detailCity" value="" disabled /> <input type="text" class="form-control" id="detailCity" value="" disabled />
</div> </div>
<div class="col-md-6"> <div class="col-md-5">
<label class="form-label" for="detailStreet">@T["Street"]</label> <label class="form-label" for="detailStreet">@T["Street"]</label>
<input type="text" class="form-control" id="detailStreet" value="" disabled /> <input type="text" class="form-control" id="detailStreet" value="" disabled />
</div> </div>
<div class="col-md-2"> <div class="col-md-3">
<label class="form-label" for="detailStreetNr">@T["Street Nr."]</label> <label class="form-label" for="detailStreetNr">@T["Street Nr."]</label>
<input type="text" class="form-control" id="detailStreetNr" value="" disabled /> <input type="text" class="form-control" id="detailStreetNr" value="" disabled />
</div> </div>

View File

@@ -75,7 +75,11 @@
<div class="mt-3 p-2 bg border rounded d-flex justify-content-between align-items-center" <div class="mt-3 p-2 bg border rounded d-flex justify-content-between align-items-center"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target="#presetsCollapse" data-bs-target="#presetsCollapse"
style="cursor: pointer;"> style="cursor: pointer;"
role="button"
tabindex="0"
aria-controls="presetsCollapse"
aria-label="@T["Presets. Press to toggle visibility"]">
<h4 class="fw-bold m-0">@T["Presets"]</h4> <h4 class="fw-bold m-0">@T["Presets"]</h4>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
@@ -88,34 +92,46 @@
@foreach (var preset in Model.Presets) @foreach (var preset in Model.Presets)
{ {
<div class="col-md-6"> <div class="col-md-6">
<div class="border rounded p-3 mb-3" data-preset-id="@(preset.Key)"> <div class="border rounded p-3 mb-3 h-100" data-preset-id="@(preset.Key)">
<label class="form-label" for="Presets.@(preset.Key).Key">@T["Preset name"]</label> <label class="form-label" for="Presets.@(preset.Key).Key">@T["Preset name"]</label>
<input class="form-control mb-3" id="Presets.@(preset.Key).Key" name="Presets.@(preset.Key).Key" value="@preset.Key"/> <input class="form-control mb-3" id="Presets.@(preset.Key).Key" name="Presets.@(preset.Key).Key" value="@preset.Key"/>
<label class="form-label">@T["Attributes"]</label> <label class="form-label">@T["Attributes"]</label>
<a href="#Presets.@(preset.Key).Delete" class="visually-hidden-focusable">@T["Skip attributes table"]</a>
<table class="attributes mb-2">
<thead>
<tr>
<th class="p-1">@T["Name"]</th>
<th class="p-1">@T["Value"]</th>
<th class="p-1">@T["Delete attribute"]</th>
</tr>
</thead>
<tbody class="attributes">
@foreach (var attr in preset.Value.Attribute) @foreach (var attr in preset.Value.Attribute)
{ {
<div class="row mb-2" data-type="attribute"> <tr data-type="attribute">
<div class="col-md-3 mt-2"> <td class="col-md-3 p-1">
<input class="form-control" <input class="form-control"
name="Presets.@(preset.Key).Attribute.@(attr.Key).Key" name="Presets.@(preset.Key).Attribute.@(attr.Key).Key"
value="@attr.Key" placeholder="@T["Name"]" aria-label="@T["Name"]" /> value="@attr.Key" placeholder="@T["Name"]" aria-label="@T["Name"]" />
</div> </td>
<div class="col-md-5 mt-2"> <td class="col-md-5 p-1">
<input class="form-control" <input class="form-control"
name="Presets.@(preset.Key).Attribute.@(attr.Key).Value" name="Presets.@(preset.Key).Attribute.@(attr.Key).Value"
value="@attr.Value" placeholder="@T["Value"]" aria-label="@T["Value"]" /> value="@attr.Value" placeholder="@T["Value"]" aria-label="@T["Value"]" />
</div> </td>
<div class="col-md-4 mt-2"> <td class="col-md-4 p-1">
<button type="button" class="btn btn-danger" data-type="deleteAttribute">@T["Delete attribute"]</button> <button type="button" class="btn btn-danger" data-type="deleteAttribute">@T["Delete attribute"]</button>
</div> </td>
</div> </tr>
} }
</tbody>
</table>
<div class="attributes"></div> <div class="attributes"></div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-8 mt-2"> <div class="col-md-6 mt-2">
<button type="button" class="btn btn-danger" data-type="deletePreset">@T["Delete preset"]</button> <button type="button" class="btn btn-danger" id="Presets.@(preset.Key).Delete" data-type="deletePreset">@T["Delete preset"]</button>
</div> </div>
<div class="col-md-4 ms-auto mt-2"> <div class="col-md-6 ms-auto mt-2">
<button type="button" class="btn btn-primary" data-type="addAttribute">@T["Add attribute"]</button> <button type="button" class="btn btn-primary" data-type="addAttribute">@T["Add attribute"]</button>
</div> </div>
</div> </div>
@@ -165,6 +181,26 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const toggle = document.querySelector('[data-bs-target="#presetsCollapse"]');
toggle.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle.click();
const collapse = document.querySelector("#presetsCollapse");
const isShown = collapse.classList.contains("show");
toggle.setAttribute("aria-expanded", isShown ? "true" : "false");
}
});
toggle.addEventListener("click", () => {
const collapse = document.querySelector("#presetsCollapse");
const isShown = collapse.classList.contains("show");
toggle.setAttribute("aria-expanded", isShown ? "true" : "false");
});
const updateForm = document.getElementById('updateSettings'); const updateForm = document.getElementById('updateSettings');
updateForm.addEventListener('submit', async e => { updateForm.addEventListener('submit', async e => {
e.preventDefault(); e.preventDefault();
@@ -216,25 +252,25 @@
function addAttribute(presetContainer, presetKey) { function addAttribute(presetContainer, presetKey) {
const attributeId = crypto.randomUUID(); const attributeId = crypto.randomUUID();
const row = document.createElement('div'); const row = document.createElement('tr');
row.classList.add('row', 'mb-2'); row.classList.add('row', 'mb-2');
row.innerHTML = ` row.innerHTML = `
<div class="col-md-3"> <td class="col-md-3 p-1">
<input class="form-control" <input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Key" name="Presets.${presetKey}.Attribute.${attributeId}.Key"
value="" /> value="" />
</div> </td>
<div class="col-md-5"> <td class="col-md-5 p-1">
<input class="form-control" <input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Value" name="Presets.${presetKey}.Attribute.${attributeId}.Value"
value="" /> value="" />
</div> </td>
<div class="col-md-4"> <td class="col-md-4 p-1">
<button type="button" class="btn btn-danger btn-delete-attr"> <button type="button" class="btn btn-danger btn-delete-attr">
${presetContainer.dataset.deleteAttributeText} ${presetContainer.dataset.deleteAttributeText}
</button> </button>
</div> </td>
`; `;
presetContainer.querySelector('.attributes').appendChild(row); presetContainer.querySelector('.attributes').appendChild(row);
@@ -311,29 +347,32 @@
const attributeId = crypto.randomUUID(); const attributeId = crypto.randomUUID();
const row = document.createElement('div'); const row = document.createElement('tr');
row.classList.add('row', 'mb-2'); row.classList.add('mb-2');
row.setAttribute('data-type', 'attribute'); row.setAttribute('data-type', 'attribute');
row.innerHTML = ` row.innerHTML = `
<div class="col-md-3"> <td class="col-md-3 p-1">
<input class="form-control" <input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Key" name="Presets.${presetKey}.Attribute.${attributeId}.Key"
value="" /> value="" />
</div> </td>
<div class="col-md-5"> <td class="col-md-5 p-1">
<input class="form-control" <input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Value" name="Presets.${presetKey}.Attribute.${attributeId}.Value"
value="" /> value="" />
</div> </td>
<div class="col-md-4"> <td class="col-md-4 p-1">
<button type="button" class="btn btn-danger" data-type="deleteAttribute"> <button type="button" class="btn btn-danger" data-type="deleteAttribute">
${presetContainer.dataset.deleteAttributeText || 'Delete attribute'} ${presetContainer.dataset.deleteAttributeText || 'Delete attribute'}
</button> </button>
</div> </td>
`; `;
attributesContainer.appendChild(row); attributesContainer.appendChild(row);
requestAnimationFrame(() => {
row.querySelector(".col-md-3 input").focus();
});
// Delete-Button Eventlistener aktivieren // Delete-Button Eventlistener aktivieren
const deleteButton = row.querySelector('button[data-type="deleteAttribute"]'); const deleteButton = row.querySelector('button[data-type="deleteAttribute"]');
@@ -350,7 +389,7 @@
function addDeletePresetEventListener(button) { function addDeletePresetEventListener(button) {
button.addEventListener('click', e => { button.addEventListener('click', e => {
const presetContainer = e.target.closest('.col-md-6'); const presetContainer = e.target.parentElement.parentElement.closest('.col-md-6');
if (!presetContainer) return; if (!presetContainer) return;
presetContainer.remove(); presetContainer.remove();
}); });
@@ -368,17 +407,27 @@
const presetDiv = document.createElement('div'); const presetDiv = document.createElement('div');
presetDiv.classList.add('col-md-6'); presetDiv.classList.add('col-md-6');
presetDiv.innerHTML = ` presetDiv.innerHTML = `
<div class="border rounded p-3 mb-3" data-preset-id="${presetId}"> <div class="border rounded p-3 mb-3 h-100" data-preset-id="${presetId}">
<label class="form-label" for="Presets.${presetId}.Key">@T["Preset name"]</label> <label class="form-label" for="Presets.${presetId}.Key">@T["Preset name"]</label>
<input class="form-control mb-3" id="Presets.${presetId}.Key" name="Presets.${presetId}.Key" value=""/> <input class="form-control mb-3" id="Presets.${presetId}.Key" name="Presets.${presetId}.Key" value=""/>
<label class="form-label">@T["Attributes"]</label> <label class="form-label">@T["Attributes"]</label>
<div class="attributes"></div> <a href="#Presets.${presetId}.Delete" class="visually-hidden-focusable">@T["Skip attributes table"]</a>
<table class="mb-2">
<thead>
<tr>
<th class="p-1">@T["Name"]</th>
<th class="p-1">@T["Value"]</th>
<th class="p-1">@T["Delete attribute"]</th>
</tr>
</thead>
<tbody class="attributes"></tbody>
</table>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-4"> <div class="col-md-6">
<button type="button" class="btn btn-danger" data-type="deletePreset">Delete preset</button> <button type="button" class="btn btn-danger" id="Presets.${presetId}.Delete" data-type="deletePreset">@T["Delete preset"]</button>
</div> </div>
<div class="col-md-4 ms-auto"> <div class="col-md-6 ms-auto">
<button type="button" class="btn btn-primary" data-type="addAttribute">Add attribute</button> <button type="button" class="btn btn-primary" data-type="addAttribute">@T["Add attribute"]</button>
</div> </div>
</div> </div>
</div> </div>
@@ -386,7 +435,9 @@
// Einfach ans Ende der Container-Liste hängen // Einfach ans Ende der Container-Liste hängen
presetsButton.parentElement.parentElement.insertBefore(presetDiv, presetsButton.parentElement); presetsButton.parentElement.parentElement.insertBefore(presetDiv, presetsButton.parentElement);
requestAnimationFrame(() => {
presetDiv.querySelector(".form-control.mb-3").focus();
});
// Eventlistener für den neuen Preset-Block aktivieren // Eventlistener für den neuen Preset-Block aktivieren
const addAttrBtn = presetDiv.querySelector('button[data-type="addAttribute"]'); const addAttrBtn = presetDiv.querySelector('button[data-type="addAttribute"]');
const deletePresetBtn = presetDiv.querySelector('button[data-type="deletePreset"]'); const deletePresetBtn = presetDiv.querySelector('button[data-type="deletePreset"]');

View File

@@ -5,7 +5,6 @@
@inject IConfiguration Configuration @inject IConfiguration Configuration
@{ @{
ViewData["Title"] = T["User settings"]; ViewData["Title"] = T["User settings"];
string barcodeType = Configuration["BarcodeType"] ?? "EAN13";
} }
@@ -33,29 +32,37 @@
</div> </div>
<h4 class="fw-bold">@T["Personal data"]</h4> <h4 class="fw-bold">@T["Personal data"]</h4>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-1"> <div class="col-md-2">
<label class="form-label" for="title">@T["Title"]</label> <label class="form-label" for="title">@T["Title"]</label>
<input type="text" id="title" class="form-control" value="@Model.userModel.Title" readonly/> <input type="text" id="title" class="form-control" value="@Model.userModel.Title" readonly/>
</div> </div>
<div class="col-md-4"> <div class="col-md-5">
<label class="form-label" for="name">@T["Name"]</label> <label class="form-label" for="name">@T["Name"]</label>
<input type="text" id="name" class="form-control" value="@Model.userModel.Cn" readonly/> <input type="text" id="name" class="form-control" value="@Model.userModel.Cn" readonly/>
</div> </div>
<div class="col-md-4"> <div class="col-md-5">
<label class="form-label" for="surname">@T["Surname"]</label> <label class="form-label" for="surname">@T["Surname"]</label>
<input type="text" id="surname" class="form-control" value="@Model.userModel.Sn" readonly/> <input type="text" id="surname" class="form-control" value="@Model.userModel.Sn" readonly/>
</div> </div>
</div> </div>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-2"> <div class="col-md-4">
<label class="form-label" for="detailCity">@T["City"]</label>
<input type="text" class="form-control" id="detailCity" value="@Model.userModel.Description?.Address.City" readonly />
</div>
<div class="col-md-5">
<label class="form-label" for="detailStreet">@T["Street"]</label>
<input type="text" class="form-control" id="detailStreet" value="@Model.userModel.Description?.Address.Street" readonly />
</div>
<div class="col-md-3">
<label class="form-label" for="detailStreetNr">@T["Street Nr."]</label>
<input type="text" class="form-control" id="detailStreetNr" value="@Model.userModel.Description?.Address.StreetNr" readonly />
</div>
<div class="col-md-3">
<label class="form-label" for="birthdate">@T["Birth date"]</label> <label class="form-label" for="birthdate">@T["Birth date"]</label>
<input type="text" id="birthdate" class="form-control" value="@Model.userModel.Description?.BirthDate" readonly/> <input type="text" id="birthdate" class="form-control" value="@Model.userModel.Description?.BirthDate" readonly/>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label" for="address">@T["Address"]</label>
<input type="text" id="address" class="form-control" value="@Model.userModel.Description?.BirthDate" readonly/>
</div>
<div class="col-md-3">
<label class="form-label" for="workplace">@T["Workplace"]</label> <label class="form-label" for="workplace">@T["Workplace"]</label>
<input type="text" id="workplace" class="form-control" value="@Model.userModel.Description?.Workplace" readonly/> <input type="text" id="workplace" class="form-control" value="@Model.userModel.Description?.Workplace" readonly/>
</div> </div>
@@ -82,6 +89,11 @@
showToast('@T["Password must be at least 8 characters long and include upper, lower, number, and special character"]', 'danger'); showToast('@T["Password must be at least 8 characters long and include upper, lower, number, and special character"]', 'danger');
return; return;
} }
const photo = updateForm.querySelector('#updatePhotoFile').value;
if (password.length == 0 && photo.length == 0) {
showToast('@T["There are no changes to be saved."]', 'danger');
return;
}
const url = `/Settings/User`; const url = `/Settings/User`;
const dataFromEntries = Object.fromEntries(new FormData(updateForm).entries()); const dataFromEntries = Object.fromEntries(new FormData(updateForm).entries());
var data = unflatten(dataFromEntries); var data = unflatten(dataFromEntries);

View File

@@ -67,7 +67,10 @@
const asset = json.assetsModel; const asset = json.assetsModel;
assetCard.innerHTML = ` assetCard.innerHTML = `
<div class="card-body" data-cn="${asset.Cn}"> <div class="card-body" data-cn="${asset.Cn}">
<h4 class="card-title mb-4" style="text-align: center;"><button class="btn btn-sm btn-danger" data-action="remove" data-batchid="${i}" style="float:left;">X</button><strong>Asset ${i + 1}:</strong> ${asset.Name}</h4> <span>
<h4 class="card-title mb-4" style="text-align: center;"><strong aria-label="Asset ${i + 1}: ${asset.Name}">Asset ${i + 1}:</strong> ${asset.Name}</h4>
<button class="btn btn-sm btn-danger" data-action="remove" data-batchid="${i}" aria-label="@T["Delete entry"]" style="float:left;">X</button>
</span>
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-5">
<p><strong>@T["Asset ID"]:</strong> ${asset.Cn}</p> <p><strong>@T["Asset ID"]:</strong> ${asset.Cn}</p>
@@ -87,7 +90,10 @@
} else { } else {
assetCard.innerHTML = ` assetCard.innerHTML = `
<div class="card-body"> <div class="card-body">
<h4 class="card-title mb-4" style="text-align: center;"><button class="btn btn-sm btn-danger" data-action="remove" data-batchid="${i}" style="float:left;">X</button><strong>Asset ${i + 1}:</strong> @T["Empty"]</h4> <span>
<h4 class="card-title mb-4" style="text-align: center;"><strong aria-label="Asset ${i + 1}: @T["Empty"]">Asset ${i + 1}:</strong> @T["Empty"]</h4>
<button class="btn btn-sm btn-danger" data-action="remove" data-batchid="${i}" aria-label="@T["Delete entry"]" style="float:left;">X</button>
</span>
<div class="row"> <div class="row">
<div id="printPreviewBatchButtons${i}" class="col-md-1 justify-content-end" style="margin-left: auto; width: auto;"> <div id="printPreviewBatchButtons${i}" class="col-md-1 justify-content-end" style="margin-left: auto; width: auto;">
</div> </div>
@@ -236,10 +242,14 @@
let cardAtIndex = document.querySelector(`[data-card-index="${index}"]`) let cardAtIndex = document.querySelector(`[data-card-index="${index}"]`)
let cardBodyAtIndex = cardAtIndex.children[0]; let cardBodyAtIndex = cardAtIndex.children[0];
cardBodyAtIndex.children[0].children[1].textContent = `Asset ${newIndex + 1}`; cardBodyAtIndex.children[1].children[0].children[0].children[0].nextSibling.textContent = ` ${newIndex + 1}`;
cardBodyAtIndex.children[0].children[0].children[0].textContent = `Asset ${newIndex + 1}:`;
cardBodyAtIndex.children[0].children[0].children[0].ariaLabel = cardBodyAtIndex.children[0].children[0].children[0].ariaLabel.replace(/\s\d+\:/, ` ${newIndex + 1}:`);
let cardAtTarget = document.querySelector(`[data-card-index="${newIndex}"]`) let cardAtTarget = document.querySelector(`[data-card-index="${newIndex}"]`)
let cardBodyAtTarget = cardAtTarget.children[0]; let cardBodyAtTarget = cardAtTarget.children[0];
cardBodyAtTarget.children[0].children[1].textContent = `Asset ${index - 0 + 1}`; cardBodyAtTarget.children[1].children[0].children[0].children[0].nextSibling.textContent = ` ${index - 0 + 1}`;
cardBodyAtTarget.children[0].children[0].children[0].textContent = `Asset ${index - 0 + 1}:`;
cardBodyAtTarget.children[0].children[0].children[0].ariaLabel = cardBodyAtTarget.children[0].children[0].children[0].ariaLabel.replace(/\s\d+\:/, ` ${newIndex + 1}:`);
cardAtIndex.insertBefore(cardBodyAtTarget, null); cardAtIndex.insertBefore(cardBodyAtTarget, null);
cardAtTarget.insertBefore(cardBodyAtIndex, null); cardAtTarget.insertBefore(cardBodyAtIndex, null);

View File

@@ -1,8 +1,11 @@
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer T
<button id="openPrintModal" <button id="openPrintModal"
class="btn btn-primary position-fixed bottom-0 start-0 m-4" class="btn btn-primary position-fixed bottom-0 start-0 m-4"
style="width: 3rem; height: 3rem;z-index: 1000;" style="width: 3rem; height: 3rem;z-index: 1000;"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#printModal" data-bs-target="#printModal"
title="Open Print Page"> title="@T["Open Print Page"]">
🖨️ 🖨️
</button> </button>

View File

@@ -1,11 +1,13 @@
@using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.AspNetCore.Mvc.Localization
@using System.Security.Claims @using System.Security.Claims
@using System.Globalization
@inject IViewLocalizer T @inject IViewLocalizer T
@{ @{
bool IsAuthenticated = User.Identity?.IsAuthenticated ?? false; bool IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="@CultureInfo.CurrentUICulture.TwoLetterISOLanguageName">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="Hardware asset management tool" /> <meta name="description" content="Hardware asset management tool" />
@@ -39,7 +41,8 @@
selectUser: '@T["Select user"]', selectUser: '@T["Select user"]',
selectPreset: '@T["Select preset"]', selectPreset: '@T["Select preset"]',
errorLoadingUsers: '@T["Error loading users"]', errorLoadingUsers: '@T["Error loading users"]',
errorLoadingPresets: '@T["Error loading presets"]' errorLoadingPresets: '@T["Error loading presets"]',
closeAlert: '@T["Close alert"]'
}; };
</script> </script>
</head> </head>
@@ -133,9 +136,92 @@
<footer class="border-top footer text-muted"> <footer class="border-top footer text-muted">
<div class="container"> <div class="container">
&copy; 2025 - Berufsschule_HAM &copy; 2025 - Berufsschule_HAM |
<a asp-controller="Home" asp-action="Accessibility">@T["Accessibility"]</a>
</div> </div>
</footer> </footer>
<!-- Re-authentication Modal -->
<div class="modal fade" id="reAuthModal" tabindex="-1" aria-labelledby="reAuthModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-info text-dark">
<h3 class="modal-title" id="detailModalLabel">@T["Extend session duration"]</h3>
<button type="button" class="btn-close btn-close-white" style="filter: invert(0);" aria-label="Close" data-bs-dismiss="modal"></button>
</div>
<form id="reAuthForm">
<div class="modal-body">
<p id="reAuthModalLabel" class="text-center">@T["The session expires soon."]<br/>@T["Please authenticate to continue without losing data."]</p>
<div class="mb-3">
<label for="reAuthUsername" class="form-label">@T["Username"]</label>
<input type="text" class="form-control" id="reAuthUsername" name="Username"
value="@User.Identity?.Name" readonly />
</div>
<div class="mb-3">
<label for="reAuthPassword" class="form-label">@T["Password"]</label>
<input type="password" class="form-control" id="reAuthPassword" name="Password" required />
</div>
<div class="alert alert-danger d-none" id="reAuthError"></div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">@T["Re-authenticate"]</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Flag so modal only opens once
window.reAuthModalOpen = false;
function openReAuthModal() {
window.reAuthModalOpen = true;
const reAuthModal = new bootstrap.Modal(document.getElementById('reAuthModal'));
reAuthModal.show();
}
function scheduleReAuthModal() {
fetch('/Home/RemainingTime')
.then(res => res.json())
.then(data => {
let remainingMinutesThreshold = 20;
let remainingMinutes = data.remainingMinutes;
let triggerMinutes = Math.max(0, remainingMinutes - remainingMinutesThreshold);
let triggerMs = triggerMinutes * 60 * 1000;
setTimeout(() => {
if (!window.reAuthModalOpen) {
openReAuthModal();
}
}, triggerMs);
})
.catch(console.error);
}
scheduleReAuthModal();
$('#reAuthForm').on('submit', function (e) {
e.preventDefault();
const formData = $(this).serialize();
$.post('/Home/Login', formData)
.done(function () {
window.reAuthModalOpen = false;
$('#reAuthModal').modal('hide');
})
.fail(function (data) {
console.log(data);
const parser = new DOMParser();
const doc = parser.parseFromString(data.responseText, 'text/html');
const errorHeading = doc.querySelector("main div div h2");
let responseText = data.responseText;
document.getElementById("reAuthError").textContent = errorHeading.textContent;
$('#reAuthError').removeClass('d-none');
});
});
});
</script>
@* <script src="~/lib/jquery/dist/jquery.min.js" defer></script> *@ @* <script src="~/lib/jquery/dist/jquery.min.js" defer></script> *@
<script src="https://code.jquery.com/jquery-3.7.1.min.js" <script src="https://code.jquery.com/jquery-3.7.1.min.js"
crossorigin="anonymous" defer></script> crossorigin="anonymous" defer></script>

View File

@@ -0,0 +1,16 @@
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer T
<form method="post" action="/Home/Login" class="mt-4" style="max-width: 400px; margin: auto;">
<div class="form-group mb-3">
<label for="username" class="form-label">@T["Username"]</label>
<input autofocus type="text" class="form-control" id="username" name="username" autocomplete="username" required>
</div>
<div class="form-group mb-3">
<label for="password" class="form-label">@T["Password"]</label>
<input type="password" class="form-control" id="password" name="password" autocomplete="current-password" required>
</div>
<button type="submit" class="btn btn-primary w-100">@T["Login"]</button>
</form>

View File

@@ -83,9 +83,34 @@ h4.fw-bold, h4.card-title {
outline: none; outline: none;
} }
input[readonly] { [data-bs-theme="dark"] input[readonly] {
background-color: #343a40 !important; background-color: #343a40 !important;
box-shadow: none; box-shadow: none;
opacity: 1; opacity: 1;
border-color: var(--bs-border-color) !important; border-color: var(--bs-border-color) !important;
} }
[data-bs-theme="light"] input[readonly] {
background-color: var(--bs-secondary-bg) !important;
box-shadow: none;
opacity: 1;
border-color: var(--bs-border-color) !important;
}
/* Groups view checkmarks */
td.text-success span, td.text-danger span {
font-size: 2rem;
}
/* Make form-check-label WCAG 2.2 (2.5.5) compliant */
.form-check {
display: flex;
align-items: center;
}
.form-check-label {
display: flex;
align-items: center;
min-height: 44px;
padding-left: 0.5rem;
}

View File

@@ -2,6 +2,16 @@
const container = document.createElement('div'); const container = document.createElement('div');
container.id = 'toastContainer'; container.id = 'toastContainer';
container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; container.className = 'toast-container position-fixed bottom-0 end-0 p-3';
container.setAttribute("aria-live", "polite");
container.setAttribute("aria-atomic", "true");
const liveRegion = document.createElement('div');
liveRegion.id = 'toastLiveRegion';
liveRegion.className = 'visually-hidden';
liveRegion.setAttribute('aria-live', 'assertive');
liveRegion.setAttribute('aria-atomic', 'true');
container.appendChild(liveRegion);
document.body.appendChild(container); document.body.appendChild(container);
return container; return container;
} }
@@ -12,14 +22,26 @@ function showToast(message, type) {
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`; toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.role = 'alert'; toast.role = 'alert';
var useDarkElements = type === "warning"
toast.innerHTML = ` toast.innerHTML = `
<div class="d-flex"> <div class="d-flex">
<div class="toast-body">${message}</div> <div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> <button type="button" class="btn-close${useDarkElements ? "" : " btn-close-white"} me-2 m-auto"${useDarkElements ? ' style="filter: unset;"' : ""} data-bs-dismiss="toast" aria-label="${window.appTranslations.closeAlert}"></button>
</div> </div>
`; `;
if (useDarkElements) {
toast.classList.remove("text-white");
toast.classList.add("text-dark");
}
toastContainer.appendChild(toast); toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
const liveRegion = document.getElementById('toastLiveRegion');
if (liveRegion) {
liveRegion.textContent = '';
setTimeout(() => liveRegion.textContent = message, 500);
}
const bsToast = new bootstrap.Toast(toast, { delay: 10000 });
bsToast.show(); bsToast.show();
toast.addEventListener('hidden.bs.toast', () => toast.remove()); toast.addEventListener('hidden.bs.toast', () => toast.remove());
} }