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" })]
public async Task<IActionResult> UserPhotoAsync(string uid, int? size)
{
Task<AdminSettingsModel> adminSettingsModelTask = _ldap.GetAdminSettingsModelAsync();
UserModel? user = await _ldap.GetUserByUidAsync(uid, _ldap.UsersAttributes);
if (user is null || user.JpegPhoto is null || user.JpegPhoto == "")
{
@@ -110,7 +109,7 @@ public class HomeController : Controller
}
if (size is not null)
{
AdminSettingsModel adminSettingsModel = await adminSettingsModelTask;
AdminSettingsModel adminSettingsModel = await _ldap.GetAdminSettingsModelAsync();
size = Math.Min((int)size, adminSettingsModel.MaxDownloadableUserImageSize);
}
byte[] encodedFile = ImageHelper.ResizeAndConvertToWebp(user.JpegPhoto, size ?? 32);
@@ -167,6 +166,7 @@ public class HomeController : Controller
return RedirectToAction("Index", "Home");
}
Response.StatusCode = 500;
switch (authenticationResult.AuthenticationState)
{
case UserNotAuthenticatedReason.InvalidCredentials:
@@ -194,9 +194,29 @@ public class HomeController : Controller
return RedirectToAction("Index", "Home");
}
[HttpGet("Accessibility")]
public ActionResult Accessibility()
{
return View();
}
[HttpGet("AccessDenied")]
public ActionResult AccessDenied()
{
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;
jpegPhoto ??= ImageHelper.GetDefaultUserImageAsBase64();
string uid = UsersHelper.CreateUsername(requestModel.Cn ?? "", requestModel.Sn ?? "");
title ??= "";
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
if (!userPassword.StartsWith('{'))
{
@@ -88,12 +87,13 @@ public class UsersController : Controller
new LdapAttribute("objectClass", "inetOrgPerson"),
new LdapAttribute("cn", requestModel.Cn),
new LdapAttribute("sn", requestModel.Sn),
new LdapAttribute("title", title),
new LdapAttribute("uid", uid),
new LdapAttribute("jpegPhoto", jpegPhoto),
new LdapAttribute("description", JsonSerializer.Serialize(description)),
new LdapAttribute("userPassword", userPassword),
];
if (title is not null && title.Length > 0) attributeSet.Add(new LdapAttribute("title", title));
await _ldap.CreateUser(uid, attributeSet);
return new(){Success = true, Uid = uid};
}
@@ -122,7 +122,7 @@ public class UsersController : Controller
await _ldap.UpdateUser(uid, "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);
}

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">
<value>Vorlage anwenden</value>
</data>
<data name="Jump to add attribute button" xml:space="preserve">
<value>Zu Attribut Hinzufügen Button springen</value>
</data>
</root>

View File

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

View File

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

View File

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

View File

@@ -19,12 +19,6 @@
<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>
<data name="Invalid login credentials" xml:space="preserve">
<value>Ungültige Anmeldedaten</value>
</data>

View File

@@ -110,4 +110,10 @@
<data name="Clear user image cache" xml:space="preserve">
<value>Cache leeren</value>
</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>

View File

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

View File

@@ -73,4 +73,7 @@
<data name="Serial" xml:space="preserve">
<value>Seriennummer</value>
</data>
<data name="Delete entry" xml:space="preserve">
<value>Eintrag löschen</value>
</data>
</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">
<value>Anmelden</value>
</data>
<data name="Accessibility" xml:space="preserve">
<value>Barrierefreiheit</value>
</data>
<data name="Select location" xml:space="preserve">
<value>Ort auswählen</value>
</data>
@@ -52,10 +55,37 @@
<data name="Select preset" xml:space="preserve">
<value>Vorlage auswählen</value>
</data>
<data name="Close alert" xml:space="preserve">
<value>Meldung schließen</value>
</data>
<data name="User settings" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="Admin settings" xml:space="preserve">
<value>Administration</value>
</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>

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

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">
<thead>
<tr>
<th>@T["Owner"]</th>
<th>@T["Asset ID"]</th>
<th>@T["Asset Name"]</th>
<th>@T["Location"]</th>
<th id="col-owner">@T["Owner"]</th>
<th id="col-assetId">@T["Asset ID"]</th>
<th id="col-assetName">@T["Asset Name"]</th>
<th id="col-assetLocation">@T["Location"]</th>
<th class="text-center">@T["Action"]</th>
</tr>
<tr>
<th><input 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 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-owner" type="text" class="form-control form-control-sm column-filter" placeholder="@T["Owner"]" data-column="0" /></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 aria-labelledby="col-assetName" type="text" class="form-control form-control-sm column-filter" placeholder="@T["Asset Name"]" data-column="2" /></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>
</tr>
</thead>
@@ -48,7 +48,7 @@
@{
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.AssetCn</td>
<td>@assetsTableViewModel.AssetName</td>
@@ -228,9 +228,18 @@
<div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="fw-bold mb-0">@T["Attributes"]</h4>
</div>
<div id="attributesContainer" class="d-flex flex-column gap-2">
<!-- Dynamic attribute rows will appear here -->
</div>
<a href="#addAttributeBtn" 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="attributesContainer">
</tbody>
</table>
<button type="button" class="btn btn-sm btn-primary mt-3" id="addAttributeBtn">
@T["Add Attribute"]
</button>
@@ -298,12 +307,19 @@ document.addEventListener('DOMContentLoaded', () => {
const addAttributeBtn = document.getElementById('addAttributeBtn');
addAttributeBtn.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center attribute-row';
const row = document.createElement('tr');
row.className = 'attribute-row';
row.classList.add("mb-3");
row.innerHTML = `
<td class="col-md-5 p-1">
<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>
</td>
`;
attributesContainer.appendChild(row);
});
@@ -364,7 +380,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (tableBody) {
const newRow = document.createElement('tr');
newRow.innerHTML = `
<td>${jsonData.Owner || ''}</td>
<td tabindex="0">${jsonData.Owner || ''}</td>
<td>${result.assetId || ''}</td>
<td>${jsonData.Name || ''}</td>
<td>${jsonData.Location || ''}</td>
@@ -463,7 +479,18 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="fw-bold mb-0">@T["Attributes"]</h4>
</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">
@T["Add Attribute"]
</button>
@@ -510,12 +537,21 @@ document.addEventListener('DOMContentLoaded', () => {
let assetId = null;
addAttrBtn.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center attribute-row';
const row = document.createElement('tr');
row.className = 'attribute-row mb-3';
let randomId = crypto.randomUUID();
row.innerHTML = `
<input type="text" class="form-control" placeholder="@T["Attribute name"]" aria-label="@T["Attribute name"]" data-attr-name />
<input type="text" class="form-control" placeholder="@T["Attribute value"]" aria-label="@T["Attribute value"]" data-attr-value />
<td class="col-md-5 p-1">
<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>
</td>
`;
updateAttributesContainer.appendChild(row);
});
@@ -559,12 +595,21 @@ document.addEventListener('DOMContentLoaded', () => {
// Attributes
if (asset.Description.Attributes) {
for (const [attrName, attrValue] of Object.entries(asset.Description.Attributes)) {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center attribute-row';
const row = document.createElement('tr');
let randomId = crypto.randomUUID();
row.className = 'attribute-row mb-3';
row.innerHTML = `
<input type="text" class="form-control" value="${attrName}" aria-label="@T["Attribute name"]" data-attr-name />
<input type="text" class="form-control" value="${attrValue}" aria-label="@T["Attribute value"]" data-attr-value />
<td class="col-md-5 p-1">
<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>
</td>
`;
updateAttributesContainer.appendChild(row);
}
@@ -672,7 +717,7 @@ document.addEventListener('DOMContentLoaded', () => {
.asset-row > td {
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;
}
</style>
@@ -686,9 +731,17 @@ document.addEventListener('DOMContentLoaded', () => {
});
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 viewContent = document.getElementById('viewAssetContent');
row.addEventListener('click', async (e) => {
// Avoid clicks on buttons inside the row
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>`;
return;
}
let i = 0;
const html = `
<div class="row g-3">
<h4 class="fw-bold">@T["Barcode"]</h4>
<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 class="col-md-6">
<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">
<h4 class="fw-bold">@T["Inventory"]</h4>
<div class="col-md-6">
<label class="form-label">@T["Name"]</label>
<input type="text" class="form-control" name="Name" value="${asset.Description.Inventory.PersonUid || ''}" disabled />
<label for="detailUsername" class="form-label">@T["Name"]</label>
<input id="detailUsername" type="text" class="form-control" name="Name" value="${asset.Description.Inventory.PersonUid || ''}" disabled />
</div>
<div class="col-md-6">
<label 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 />
<label for="detailDate" class="form-label">@T["Date"]</label>
<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>
<hr class="my-3" />
<div class="row g-3">
<h4 class="fw-bold">@T["Information"]</h4>
<div class="col-md-6">
<label class="form-label">@T["Name"]</label>
<input type="text" class="form-control" name="Name" value="${asset.Name || ''}" disabled />
<label for="detailName" class="form-label">@T["Name"]</label>
<input id="detailName" type="text" class="form-control" name="Name" value="${asset.Name || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Location"]</label>
<input type="text" class="form-control" name="Location" value="${asset.Location || ''}" disabled />
<label for="detailLocation" class="form-label">@T["Location"]</label>
<input id="detailLocation" type="text" class="form-control" name="Location" value="${asset.Location || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Owner"]</label>
<input type="text" class="form-control" name="Owner" value="${asset.Owner || ''}" disabled />
<label for="detailOwner" class="form-label">@T["Owner"]</label>
<input id="detailOwner" type="text" class="form-control" name="Owner" value="${asset.Owner || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Serial Number"]</label>
<input type="text" class="form-control" name="SerialNumber" value="${asset.SerialNumber || ''}" disabled />
<label for="detailSerialnumber" class="form-label">@T["Serial Number"]</label>
<input id="detailSerialnumber" type="text" class="form-control" name="SerialNumber" value="${asset.SerialNumber || ''}" disabled />
</div>
</div>
<hr class="my-3" />
<div class="row g-3">
<h4 class="fw-bold">@T["Description"]</h4>
<div class="col-md-6">
<label class="form-label">@T["Type"]</label>
<input type="text" class="form-control" name="Description.Type" value="${asset.Description?.Type || ''}" disabled />
<label for="detailType" class="form-label">@T["Type"]</label>
<input id="detailType" type="text" class="form-control" name="Description.Type" value="${asset.Description?.Type || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Make"]</label>
<input type="text" class="form-control" name="Description.Make" value="${asset.Description?.Make || ''}" disabled />
<label for="detailMake" class="form-label">@T["Make"]</label>
<input id="detailMake" type="text" class="form-control" name="Description.Make" value="${asset.Description?.Make || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Model"]</label>
<input type="text" class="form-control" name="Description.Model" value="${asset.Description?.Model || ''}" disabled />
<label for="detailModel" class="form-label">@T["Model"]</label>
<input id="detailModel" type="text" class="form-control" name="Description.Model" value="${asset.Description?.Model || ''}" disabled />
</div>
</div>
${asset.Description?.Attributes ? `
@@ -774,8 +827,8 @@ function registerRowDetailviewClick(row) {
${Object.entries(asset.Description.Attributes)
.map(([k,v]) => `
<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}" />:
<input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value disabled value="${v}" />
<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}" />:
<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>`)
.join('')}
</div>` : ''}
@@ -785,20 +838,20 @@ function registerRowDetailviewClick(row) {
<div class="row g-3">
<h4 class="fw-bold">@T["Purchase Information"]</h4>
<div class="col-md-6">
<label class="form-label">@T["Purchase Date"]</label>
<input type="date" class="form-control" name="Description.Purchase.PurchaseDate" value="${asset.Description.Purchase.PurchaseDate || ''}" disabled />
<label for="detailPurchaseDate" class="form-label">@T["Purchase Date"]</label>
<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 class="col-md-6">
<label class="form-label">@T["Purchase Value"]</label>
<input type="text" class="form-control" name="Description.Purchase.PurchaseValue" value="${asset.Description.Purchase.PurchaseValue || ''}" disabled />
<label for="detailPurchaseValue" class="form-label">@T["Purchase Value"]</label>
<input id="detailPurchaseValue" type="text" class="form-control" name="Description.Purchase.PurchaseValue" value="${asset.Description.Purchase.PurchaseValue || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Purchased At"]</label>
<input type="text" class="form-control" name="Description.Purchase.PurchaseAt" value="${asset.Description.Purchase.PurchaseAt || ''}" disabled />
<label for="detailPurchaseAt" class="form-label">@T["Purchased At"]</label>
<input id="detailPurchaseAt" type="text" class="form-control" name="Description.Purchase.PurchaseAt" value="${asset.Description.Purchase.PurchaseAt || ''}" disabled />
</div>
<div class="col-md-6">
<label class="form-label">@T["Purchased By"]</label>
<input type="text" class="form-control" name="Description.Purchase.PurchaseBy" value="${asset.Description.Purchase.PurchaseBy || ''}" disabled />
<label for="detailPurchaseBy" class="form-label">@T["Purchased By"]</label>
<input id="detailPurchaseBy" type="text" class="form-control" name="Description.Purchase.PurchaseBy" value="${asset.Description.Purchase.PurchaseBy || ''}" disabled />
</div>
</div>` : ''}
</div>`;
@@ -822,7 +875,6 @@ function registerRowDetailviewClick(row) {
console.error(err);
viewContent.innerHTML = `<p class="text-danger text-center">@T["Error loading asset details"]</p>`;
}
});
}
@@ -876,12 +928,18 @@ document.addEventListener('DOMContentLoaded', () => {
if (anyEquals) {
anyEqualsElement.value = attributeValue;
} else {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center attribute-row';
const row = document.createElement('tr');
row.className = 'attribute-row mb-3';
row.innerHTML = `
<td class="col-md-5 p-1">
<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}" />
</td>
<td class="col-md-2 p-1">
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
</td>
`;
attributesContainer.appendChild(row);
}

View File

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

View File

@@ -52,4 +52,13 @@
}
</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>

View File

@@ -17,6 +17,7 @@
<div class="row g-3">
<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"]" />
<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>
@@ -72,9 +73,11 @@
<script defer>
async function onScanSuccess(decodedText, decodedResult) {
async function onScanSuccess(decodedText, decodedResult, isEnteredManually) {
const rawDecoded = decodedText;
const BARCODE_TYPE = "@barcodeType";
if (!isEnteredManually)
{
switch (BARCODE_TYPE.toUpperCase()) {
case "EAN13":
decodedText = decodedText.slice(0,-1);
@@ -101,6 +104,7 @@
decodedText = decodedText.slice(0,-1);
break;
}
}
decodedText = decodedText.replace(/^0+/, '');
console.log(`Code matched = ${decodedText}`, decodedResult);
document.getElementById("barcodeInput").value = decodedText;
@@ -144,19 +148,19 @@
<div class="row g-3">
<div class="col-md-6">
<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 class="col-md-6">
<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 class="col-md-6">
<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 class="col-md-6">
<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>
<hr class="my-3" />
@@ -164,28 +168,44 @@
<h4 class="fw-bold">@T["Description"]</h4>
<div class="col-md-6">
<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 class="col-md-6">
<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 class="col-md-6">
<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>
${asset.Description?.Attributes ? `
<hr class="my-3" />
<div class="row g-3">
<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)
.map(([k,v]) => `
<div class="d-flex 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}" />:
<input type="text" class="form-control" placeholder="@T["Attribute value"]" aria-label="@T["Attribute value"]" data-attr-value disabled value="${v}" />
</div>`)
<tr class="gap-2 align-items-center attribute-row">
<td class="d-flex p-1 align-items-center">
<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}" />
<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('')}
</tbody>
</table>
</div>` : ''}
${asset.Description?.Purchase ? `
@@ -194,19 +214,19 @@
<h4 class="fw-bold">@T["Purchase Information"]</h4>
<div class="col-md-6">
<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 class="col-md-6">
<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 class="col-md-6">
<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 class="col-md-6">
<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>`;
@@ -251,7 +271,7 @@
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');
@@ -385,10 +405,21 @@
</div>
<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>
<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 id="updateAttributesContainer" class="d-flex flex-column gap-2"></div>
<button type="button" class="btn btn-sm btn-primary mt-3" id="updateAddAttributeBtn">
@T["Add Attribute"]
</button>
@@ -435,12 +466,18 @@ document.addEventListener('DOMContentLoaded', () => {
let assetId = null;
addAttrBtn.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center attribute-row';
const row = document.createElement('tr');
row.className = 'attribute-row mb-3';
row.innerHTML = `
<input type="text" class="form-control" aria-label="@T["Attribute name"]" placeholder="@T["Attribute name"]" data-attr-name />
<input type="text" class="form-control" aria-label="@T["Attribute value"]" placeholder="@T["Attribute value"]" data-attr-value />
<td class="col-md-5 p-1">
<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>
</td>
`;
updateAttributesContainer.appendChild(row);
});
@@ -486,12 +523,18 @@ document.addEventListener('DOMContentLoaded', () => {
// Attributes
if (asset.Description.Attributes) {
for (const [attrName, attrValue] of Object.entries(asset.Description.Attributes)) {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center attribute-row';
const row = document.createElement('tr');
row.className = 'attribute-row mb-3';
row.innerHTML = `
<input type="text" class="form-control" aria-label="@T["Attribute name"]" value="${attrName}" data-attr-name />
<input type="text" class="form-control" aria-label="@T["Attribute value"]" value="${attrValue}" data-attr-value />
<td class="col-md-5 p-1">
<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>
</td>
`;
updateAttributesContainer.appendChild(row);
}

View File

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

View File

@@ -15,17 +15,5 @@
</div>
}
}
<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" 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>
<partial name="_Login" />
</div>

View File

@@ -176,12 +176,12 @@
<input type="text" id="updateTitle" name="Title" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label" for="updateName">@T["Name"]</label>
<input type="text" id="updateName" name="Cn" class="form-control" />
<label class="form-label" for="updateName">@T["Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="updateName" name="Cn" aria-required="true" required class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label" for="updateSurname">@T["Surname"]</label>
<input type="text" id="updateSurname" name="Sn" class="form-control" />
<label class="form-label" for="updateSurname">@T["Surname"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" id="updateSurname" name="Sn" aria-required="true" required class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label" for="updateBirthdate">@T["Birth Date"]</label>
@@ -197,7 +197,7 @@
</div>
<div class="col-md-6">
<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>
<hr class="my-3">
<h4 class="fw-bold">@T["Workplace & account"]</h4>
@@ -213,7 +213,7 @@
</div>
<div class="col-md-6">
<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 class="col-md-6">
<label class="form-label" for="updatePhotoFile">@T["Photo"]</label>
@@ -278,6 +278,9 @@
var dataFromEntries = Object.fromEntries(new FormData(updateForm).entries());
var data = unflatten(dataFromEntries);
data.Description.Groups = Array.from(updateForm.querySelector('#updateGroups').selectedOptions).map(option => option.value);
if (data.Description.Address.StreetNr == "") {
delete(data.Description.Address.StreetNr);
}
try {
const response = await fetch('/Users/Update', {
method: 'POST',
@@ -434,12 +437,12 @@
<input type="text" name="Title" id="createTitle" class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label" for="createName">@T["Name"]</label>
<input type="text" name="Cn" id="createName" class="form-control" />
<label class="form-label" for="createName">@T["Name"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" name="Cn" id="createName" aria-required="true" required class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label" for="createSurname">@T["Surname"]</label>
<input type="text" name="Sn" id="createSurname" class="form-control" />
<label class="form-label" for="createSurname">@T["Surname"] <span class="text-danger" aria-hidden="true">*</span></label>
<input type="text" name="Sn" id="createSurname" aria-required="true" required class="form-control" />
</div>
<div class="col-md-6">
<label class="form-label" for="createBirthDate">@T["Birth Date"]</label>
@@ -455,7 +458,7 @@
</div>
<div class="col-md-6">
<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>
<hr class="my-3">
<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>
</div>
<div class="col-md-6">
<label class="form-label" for="createPassword">@T["Password"]</label>
<input type="password" id="createPassword" name="UserPassword" class="form-control" />
<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" 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 class="col-md-6">
<label class="form-label" for="createPhotoFile">@T["Photo"]</label>
@@ -539,7 +542,9 @@
const dataFromEntries = Object.fromEntries(new FormData(createForm).entries());
const data = unflatten(dataFromEntries);
data.Description.Groups = Array.from(createGroupsSelect.selectedOptions).map(o => o.value);
if (data.Description.Address.StreetNr == "") {
delete(data.Description.Address.StreetNr);
}
try {
const response = await fetch('/Users/Create', {
method: 'POST',
@@ -572,7 +577,7 @@
data-user-birthdate="${data.Description.BirthDate}"
data-user-address-city="${data.Description.Address.City}"
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-groups='${JSON.stringify(data.Description?.Groups || [])}'
data-bs-toggle="modal"
@@ -580,7 +585,7 @@
@T["Update"]
</button>
<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-bs-toggle="modal"
data-bs-target="#deleteModal">
@@ -661,11 +666,11 @@
<label class="form-label" for="detailCity">@T["City"]</label>
<input type="text" class="form-control" id="detailCity" value="" disabled />
</div>
<div class="col-md-6">
<div class="col-md-5">
<label class="form-label" for="detailStreet">@T["Street"]</label>
<input type="text" class="form-control" id="detailStreet" value="" disabled />
</div>
<div class="col-md-2">
<div class="col-md-3">
<label class="form-label" for="detailStreetNr">@T["Street Nr."]</label>
<input type="text" class="form-control" id="detailStreetNr" value="" disabled />
</div>

View File

@@ -75,7 +75,11 @@
<div class="mt-3 p-2 bg border rounded d-flex justify-content-between align-items-center"
data-bs-toggle="collapse"
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>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
@@ -88,34 +92,46 @@
@foreach (var preset in Model.Presets)
{
<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>
<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>
<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)
{
<div class="row mb-2" data-type="attribute">
<div class="col-md-3 mt-2">
<tr data-type="attribute">
<td class="col-md-3 p-1">
<input class="form-control"
name="Presets.@(preset.Key).Attribute.@(attr.Key).Key"
value="@attr.Key" placeholder="@T["Name"]" aria-label="@T["Name"]" />
</div>
<div class="col-md-5 mt-2">
</td>
<td class="col-md-5 p-1">
<input class="form-control"
name="Presets.@(preset.Key).Attribute.@(attr.Key).Value"
value="@attr.Value" placeholder="@T["Value"]" aria-label="@T["Value"]" />
</div>
<div class="col-md-4 mt-2">
</td>
<td class="col-md-4 p-1">
<button type="button" class="btn btn-danger" data-type="deleteAttribute">@T["Delete attribute"]</button>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="attributes"></div>
<div class="row mb-2">
<div class="col-md-8 mt-2">
<button type="button" class="btn btn-danger" data-type="deletePreset">@T["Delete preset"]</button>
<div class="col-md-6 mt-2">
<button type="button" class="btn btn-danger" id="Presets.@(preset.Key).Delete" data-type="deletePreset">@T["Delete preset"]</button>
</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>
</div>
</div>
@@ -165,6 +181,26 @@
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');
updateForm.addEventListener('submit', async e => {
e.preventDefault();
@@ -216,25 +252,25 @@
function addAttribute(presetContainer, presetKey) {
const attributeId = crypto.randomUUID();
const row = document.createElement('div');
const row = document.createElement('tr');
row.classList.add('row', 'mb-2');
row.innerHTML = `
<div class="col-md-3">
<td class="col-md-3 p-1">
<input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Key"
value="" />
</div>
<div class="col-md-5">
</td>
<td class="col-md-5 p-1">
<input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Value"
value="" />
</div>
<div class="col-md-4">
</td>
<td class="col-md-4 p-1">
<button type="button" class="btn btn-danger btn-delete-attr">
${presetContainer.dataset.deleteAttributeText}
</button>
</div>
</td>
`;
presetContainer.querySelector('.attributes').appendChild(row);
@@ -311,29 +347,32 @@
const attributeId = crypto.randomUUID();
const row = document.createElement('div');
row.classList.add('row', 'mb-2');
const row = document.createElement('tr');
row.classList.add('mb-2');
row.setAttribute('data-type', 'attribute');
row.innerHTML = `
<div class="col-md-3">
<td class="col-md-3 p-1">
<input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Key"
value="" />
</div>
<div class="col-md-5">
</td>
<td class="col-md-5 p-1">
<input class="form-control"
name="Presets.${presetKey}.Attribute.${attributeId}.Value"
value="" />
</div>
<div class="col-md-4">
</td>
<td class="col-md-4 p-1">
<button type="button" class="btn btn-danger" data-type="deleteAttribute">
${presetContainer.dataset.deleteAttributeText || 'Delete attribute'}
</button>
</div>
</td>
`;
attributesContainer.appendChild(row);
requestAnimationFrame(() => {
row.querySelector(".col-md-3 input").focus();
});
// Delete-Button Eventlistener aktivieren
const deleteButton = row.querySelector('button[data-type="deleteAttribute"]');
@@ -350,7 +389,7 @@
function addDeletePresetEventListener(button) {
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;
presetContainer.remove();
});
@@ -368,17 +407,27 @@
const presetDiv = document.createElement('div');
presetDiv.classList.add('col-md-6');
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>
<input class="form-control mb-3" id="Presets.${presetId}.Key" name="Presets.${presetId}.Key" value=""/>
<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="col-md-4">
<button type="button" class="btn btn-danger" data-type="deletePreset">Delete preset</button>
<div class="col-md-6">
<button type="button" class="btn btn-danger" id="Presets.${presetId}.Delete" data-type="deletePreset">@T["Delete preset"]</button>
</div>
<div class="col-md-4 ms-auto">
<button type="button" class="btn btn-primary" data-type="addAttribute">Add attribute</button>
<div class="col-md-6 ms-auto">
<button type="button" class="btn btn-primary" data-type="addAttribute">@T["Add attribute"]</button>
</div>
</div>
</div>
@@ -386,7 +435,9 @@
// Einfach ans Ende der Container-Liste hängen
presetsButton.parentElement.parentElement.insertBefore(presetDiv, presetsButton.parentElement);
requestAnimationFrame(() => {
presetDiv.querySelector(".form-control.mb-3").focus();
});
// Eventlistener für den neuen Preset-Block aktivieren
const addAttrBtn = presetDiv.querySelector('button[data-type="addAttribute"]');
const deletePresetBtn = presetDiv.querySelector('button[data-type="deletePreset"]');

View File

@@ -5,7 +5,6 @@
@inject IConfiguration Configuration
@{
ViewData["Title"] = T["User settings"];
string barcodeType = Configuration["BarcodeType"] ?? "EAN13";
}
@@ -33,29 +32,37 @@
</div>
<h4 class="fw-bold">@T["Personal data"]</h4>
<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>
<input type="text" id="title" class="form-control" value="@Model.userModel.Title" readonly/>
</div>
<div class="col-md-4">
<div class="col-md-5">
<label class="form-label" for="name">@T["Name"]</label>
<input type="text" id="name" class="form-control" value="@Model.userModel.Cn" readonly/>
</div>
<div class="col-md-4">
<div class="col-md-5">
<label class="form-label" for="surname">@T["Surname"]</label>
<input type="text" id="surname" class="form-control" value="@Model.userModel.Sn" readonly/>
</div>
</div>
<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>
<input type="text" id="birthdate" class="form-control" value="@Model.userModel.Description?.BirthDate" readonly/>
</div>
<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>
<input type="text" id="workplace" class="form-control" value="@Model.userModel.Description?.Workplace" readonly/>
</div>
@@ -82,6 +89,11 @@
showToast('@T["Password must be at least 8 characters long and include upper, lower, number, and special character"]', 'danger');
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 dataFromEntries = Object.fromEntries(new FormData(updateForm).entries());
var data = unflatten(dataFromEntries);

View File

@@ -67,7 +67,10 @@
const asset = json.assetsModel;
assetCard.innerHTML = `
<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="col-md-5">
<p><strong>@T["Asset ID"]:</strong> ${asset.Cn}</p>
@@ -87,7 +90,10 @@
} else {
assetCard.innerHTML = `
<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 id="printPreviewBatchButtons${i}" class="col-md-1 justify-content-end" style="margin-left: auto; width: auto;">
</div>
@@ -236,10 +242,14 @@
let cardAtIndex = document.querySelector(`[data-card-index="${index}"]`)
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 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);
cardAtTarget.insertBefore(cardBodyAtIndex, null);

View File

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

View File

@@ -1,11 +1,13 @@
@using Microsoft.AspNetCore.Mvc.Localization
@using System.Security.Claims
@using System.Globalization
@inject IViewLocalizer T
@{
bool IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
}
<!DOCTYPE html>
<html lang="en">
<html lang="@CultureInfo.CurrentUICulture.TwoLetterISOLanguageName">
<head>
<meta charset="utf-8" />
<meta name="description" content="Hardware asset management tool" />
@@ -39,7 +41,8 @@
selectUser: '@T["Select user"]',
selectPreset: '@T["Select preset"]',
errorLoadingUsers: '@T["Error loading users"]',
errorLoadingPresets: '@T["Error loading presets"]'
errorLoadingPresets: '@T["Error loading presets"]',
closeAlert: '@T["Close alert"]'
};
</script>
</head>
@@ -133,9 +136,92 @@
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - Berufsschule_HAM
&copy; 2025 - Berufsschule_HAM |
<a asp-controller="Home" asp-action="Accessibility">@T["Accessibility"]</a>
</div>
</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="https://code.jquery.com/jquery-3.7.1.min.js"
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;
}
input[readonly] {
[data-bs-theme="dark"] input[readonly] {
background-color: #343a40 !important;
box-shadow: none;
opacity: 1;
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');
container.id = 'toastContainer';
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);
return container;
}
@@ -12,14 +22,26 @@ function showToast(message, type) {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.role = 'alert';
var useDarkElements = type === "warning"
toast.innerHTML = `
<div class="d-flex">
<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>
`;
if (useDarkElements) {
toast.classList.remove("text-white");
toast.classList.add("text-dark");
}
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();
toast.addEventListener('hidden.bs.toast', () => toast.remove());
}