diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index fc3e440..32d565c 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -98,7 +98,6 @@ public class HomeController : Controller [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "uid", "size" })] public async Task UserPhotoAsync(string uid, int? size) { - Task 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 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) }); + } + } \ No newline at end of file diff --git a/src/Controllers/UsersController.cs b/src/Controllers/UsersController.cs index 76b5378..3dd1dbc 100644 --- a/src/Controllers/UsersController.cs +++ b/src/Controllers/UsersController.cs @@ -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); } diff --git a/src/Resources/Views.Home.Accessibility.de.resx b/src/Resources/Views.Home.Accessibility.de.resx new file mode 100644 index 0000000..28c97e6 --- /dev/null +++ b/src/Resources/Views.Home.Accessibility.de.resx @@ -0,0 +1,41 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + + + + Barrierefreiheit + + + + Am {0} entspricht sämtlicher Inhalt im Projekt LD-Reborn/HAM den Web Content Accessibility Guidelines 2.2 unter {1}. Konformitätsstufe Triple-A. + + + Die Technologie, auf die sich dieser Inhalt "stützt", ist: + + + Die Technologien, die dieser Inhalt verwendet, jedoch nicht voraussetzt, sind: + + + Dieser Inhalt wurde mit den folgenden User Agents und Assistenztechnologien getestet: + + + Diese Seite enthält sowohl Text + als auch Bilder. + Alternativtexte sind für alle Bilder enthalten, und ausführliche Beschreibungen 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. + + + {0} mit {1} und {2} + + diff --git a/src/Resources/Views.Home.Accessibility.en.resx b/src/Resources/Views.Home.Accessibility.en.resx new file mode 100644 index 0000000..1372b67 --- /dev/null +++ b/src/Resources/Views.Home.Accessibility.en.resx @@ -0,0 +1,47 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + + + + Accessibility + + + + On {0}, all content available in the project LD-Reborn/HAM conforms to Web Content Accessibility Guidelines 2.2 at {1}. Triple-A conformance. + + + The technology that this content "relies upon" is: + + + The technologies that this content "uses but does not rely + upon" are: + + + This content was tested using the following user agents and assistive + technologies: + + + This page includes both text + and images. + Alternative + text is included for all image content and long + descriptions 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. + + + {0} on {1} with {2} + + diff --git a/src/Resources/Views.Home.Assets.de.resx b/src/Resources/Views.Home.Assets.de.resx index 1becde1..477173d 100644 --- a/src/Resources/Views.Home.Assets.de.resx +++ b/src/Resources/Views.Home.Assets.de.resx @@ -184,4 +184,7 @@ Vorlage anwenden + + Zu Attribut Hinzufügen Button springen + diff --git a/src/Resources/Views.Home.Groups.de.resx b/src/Resources/Views.Home.Groups.de.resx index 8314635..8d192f3 100644 --- a/src/Resources/Views.Home.Groups.de.resx +++ b/src/Resources/Views.Home.Groups.de.resx @@ -127,4 +127,10 @@ Fehler beim Anpassen der Gruppe + + Ja + + + Nein + diff --git a/src/Resources/Views.Home.Index.de.resx b/src/Resources/Views.Home.Index.de.resx index 6933252..c975086 100644 --- a/src/Resources/Views.Home.Index.de.resx +++ b/src/Resources/Views.Home.Index.de.resx @@ -40,5 +40,10 @@ Navigiere von hier oder über die Navigationsleiste - + + Einstellungen + + + Administration + diff --git a/src/Resources/Views.Home.Inventory.de.resx b/src/Resources/Views.Home.Inventory.de.resx index 859a0cd..2fdd077 100644 --- a/src/Resources/Views.Home.Inventory.de.resx +++ b/src/Resources/Views.Home.Inventory.de.resx @@ -150,4 +150,7 @@ Barcode wurde erfolgreich dem Auftragsstapel hinzugefügt + + Zu Attribut Hinzufügen Button springen + diff --git a/src/Resources/Views.Home.Login.de.resx b/src/Resources/Views.Home.Login.de.resx index d010f49..ea5d6e5 100644 --- a/src/Resources/Views.Home.Login.de.resx +++ b/src/Resources/Views.Home.Login.de.resx @@ -19,12 +19,6 @@ Anmelden - - Benutzername - - - Passwort - Ungültige Anmeldedaten diff --git a/src/Resources/Views.Settings.Admin.de.resx b/src/Resources/Views.Settings.Admin.de.resx index b8644ee..6113f56 100644 --- a/src/Resources/Views.Settings.Admin.de.resx +++ b/src/Resources/Views.Settings.Admin.de.resx @@ -110,4 +110,10 @@ Cache leeren + + Vorlagen. Klicken zum Auf- oder zuklappen + + + Attribut-Tabelle überspringen + diff --git a/src/Resources/Views.Settings.User.de.resx b/src/Resources/Views.Settings.User.de.resx index 532804f..810f844 100644 --- a/src/Resources/Views.Settings.User.de.resx +++ b/src/Resources/Views.Settings.User.de.resx @@ -64,4 +64,7 @@ Fehler bei der Kommunikation mit dem Server + + Es wurden keine Änderungen gemacht. + diff --git a/src/Resources/Views.Shared._Batch.de.resx b/src/Resources/Views.Shared._Batch.de.resx index 23cc857..4898375 100644 --- a/src/Resources/Views.Shared._Batch.de.resx +++ b/src/Resources/Views.Shared._Batch.de.resx @@ -73,4 +73,7 @@ Seriennummer + + Eintrag löschen + diff --git a/src/Resources/Views.Shared._BatchButton.resx b/src/Resources/Views.Shared._BatchButton.resx new file mode 100644 index 0000000..ec9967a --- /dev/null +++ b/src/Resources/Views.Shared._BatchButton.resx @@ -0,0 +1,20 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + + + + Druckseite öffnen + + + diff --git a/src/Resources/Views.Shared._Layout.de.resx b/src/Resources/Views.Shared._Layout.de.resx index c522130..2d81a04 100644 --- a/src/Resources/Views.Shared._Layout.de.resx +++ b/src/Resources/Views.Shared._Layout.de.resx @@ -37,6 +37,9 @@ Anmelden + + Barrierefreiheit + Ort auswählen @@ -52,10 +55,37 @@ Vorlage auswählen + + Meldung schließen + Einstellungen Administration + + Sitzung verlängern + + + Die Sitzung läuft bald ab. + + + Bitte authentifizieren Sie sich, um fortzufahren, ohne Daten zu verlieren. + + + Authentifizieren + + + Ungültige Anmeldedaten + + + Ihr Konto wurde gesperrt. Warten Sie einige Minuten oder bitten Sie einen Administrator, die Sperre aufzuheben. + + + Sie sind nicht zur Anmeldung berechtigt. Bitten Sie einen Administrator, Ihnen die Berechtigung zu erteilen. + + + Die Hölle ist zugefroren. Machen Sie einen Screenshot und senden Sie ihn an einen Administrator. + diff --git a/src/Resources/Views.Shared._Login.de.resx b/src/Resources/Views.Shared._Login.de.resx new file mode 100644 index 0000000..f3d554f --- /dev/null +++ b/src/Resources/Views.Shared._Login.de.resx @@ -0,0 +1,25 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + + + + Anmelden + + + Benutzername + + + Passwort + + diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index bb74a51..17a3f6a 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -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; @@ -29,19 +30,27 @@ public partial class LdapService : IDisposable { try { - if (!_conn.Connected) + await _connLock.WaitAsync(); + try { - try + if (!_conn.Connected) { - await _conn.ConnectAsync(_opts.Host, _opts.Port); - } - catch (SystemException ex) - { - _logger.LogWarning("Unable to connect to LDAP: {ex.Message}\n{ex.StackTrace}", [ex.Message, ex.StackTrace]); - throw; + try + { + await _conn.ConnectAsync(_opts.Host, _opts.Port); + } + catch (SystemException ex) + { + _logger.LogWarning("Unable to connect to LDAP: {ex.Message}\n{ex.StackTrace}", [ex.Message, ex.StackTrace]); + throw; + } } + await _conn.BindAsync(_opts.BindDn, _opts.BindPassword); + } + finally + { + _connLock.Release(); } - await _conn.BindAsync(_opts.BindDn, _opts.BindPassword); 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) { @@ -388,15 +397,16 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) public async Task>> ListObjectBy(string baseDn, string filter, string[] attributes) { - return await Task.Run(async () => + await ConnectAndBind(); + await _connLock.WaitAsync(); + try { - await ConnectAndBind(); var search = await _conn.SearchAsync( baseDn, LdapConnection.ScopeSub, $"{filter}", attributes, - false); + false).ConfigureAwait(false); var list = new List>(); 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) @@ -468,7 +482,15 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) string dn = PrependRDN($"{rdnKey}={rdnValue}", baseDn); if (attributeName == rdnKey) { - await _conn.RenameAsync(dn, $"{rdnKey}={attributeValue}", true); + await _connLock.WaitAsync(); + try + { + await _conn.RenameAsync(dn, $"{rdnKey}={attributeValue}", true); + } + finally + { + _connLock.Release(); + } } else { @@ -476,7 +498,7 @@ public async Task CreateAsset(LdapAttributeSet attributeSet) 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 _conn.DeleteAsync(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 _conn.AddAsync(ldapEntry); + await _connLock.WaitAsync(); + try + { + await _conn.AddAsync(ldapEntry); + } + finally + { + _connLock.Release(); + } } - public async Task ModifyAsync(string dn, LdapModification ldapModification) + public async Task ModifyAsync(string dn, LdapModification mod, CancellationToken ct = default) { - await _conn.ModifyAsync(dn, ldapModification); + await _connLock.WaitAsync(ct); + try + { + await _conn.ModifyAsync(dn, mod, ct); + } + finally + { + _connLock.Release(); + } } public void Dispose() diff --git a/src/Views/Home/Accessibility.cshtml b/src/Views/Home/Accessibility.cshtml new file mode 100644 index 0000000..450b45e --- /dev/null +++ b/src/Views/Home/Accessibility.cshtml @@ -0,0 +1,22 @@ +@using Microsoft.AspNetCore.Html +@using Microsoft.AspNetCore.Mvc.Localization +@inject IViewLocalizer T +@{ + ViewData["Title"] = T["Accessibility"]; +} + +
+

@T["PageIntro", + "28 November 2025", + new HtmlString("https://www.w3.org/TR/2024/REC-WCAG22-20241212/") + ]) +

+
    +
  • @T["ReliesUpon"] + HTML 5.
  • +
  • @T["UsesButNotReliesUpon"] CSS2.1, gif.
  • +
  • @T["TestedWith"] @T["OnWith", "Firefox 145.0.2", "Kubuntu", "Orca"] +
  • +
+

@T["Features"]

+
\ No newline at end of file diff --git a/src/Views/Home/Assets.cshtml b/src/Views/Home/Assets.cshtml index dd4ff62..bd32c4b 100644 --- a/src/Views/Home/Assets.cshtml +++ b/src/Views/Home/Assets.cshtml @@ -30,17 +30,17 @@ - - - - + + + + - - - - + + + + @@ -48,7 +48,7 @@ @{ foreach (AssetsTableViewModel assetsTableViewModel in Model.AssetsTableViewModels) { - + @@ -228,9 +228,18 @@

@T["Attributes"]

-
- -
+ @T["Jump to add attribute button"] +
@T["Owner"]@T["Asset ID"]@T["Asset Name"]@T["Location"]@T["Owner"]@T["Asset ID"]@T["Asset Name"]@T["Location"] @T["Action"]
-
@assetsTableViewModel.UserUID @assetsTableViewModel.AssetCn @assetsTableViewModel.AssetName
+ + + + + + + + + +
@T["Attribute name"]@T["Attribute value"]@T["Delete"]
@@ -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 = ` - - - + + + + + + + + + `; attributesContainer.appendChild(row); }); @@ -364,7 +380,7 @@ document.addEventListener('DOMContentLoaded', () => { if (tableBody) { const newRow = document.createElement('tr'); newRow.innerHTML = ` - ${jsonData.Owner || ''} + ${jsonData.Owner || ''} ${result.assetId || ''} ${jsonData.Name || ''} ${jsonData.Location || ''} @@ -463,7 +479,18 @@ document.addEventListener('DOMContentLoaded', () => {

@T["Attributes"]

-
+ @T["Jump to add attribute button"] + + + + + + + + + + +
@T["Attribute name"]@T["Attribute value"]@T["Delete"]
@@ -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 = ` - - - + + + + + + + + + + + `; 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 = ` - - - + + + + + + + + + + + `; 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; } @@ -686,145 +731,152 @@ document.addEventListener('DOMContentLoaded', () => { }); function registerRowDetailviewClick(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; - - const assetId = row.getAttribute('data-asset-id'); - viewContent.innerHTML = '

@T["Loading..."]

'; - - const modal = new bootstrap.Modal(viewModal); - modal.show(); - - try { - const response = await fetch(`/Assets/Get?cn=${assetId}`); - const json = await response.json(); - const asset = json.assetsModel; - - if (!asset) { - viewContent.innerHTML = `

@T["Asset not found."]

`; - return; - } - - const html = ` -
-

@T["Barcode"]

-
- -
-
- - -
-
-
-
-

@T["Inventory"]

-
- - -
-
- - -
-
-
-
-

@T["Information"]

-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-

@T["Description"]

-
- - -
-
- - -
-
- - -
-
- ${asset.Description?.Attributes ? ` -
-
-

@T["Attributes"]

- ${Object.entries(asset.Description.Attributes) - .map(([k,v]) => ` -
- : - -
`) - .join('')} -
` : ''} - - ${asset.Description?.Purchase ? ` -
-
-

@T["Purchase Information"]

-
- - -
-
- - -
-
- - -
-
- - -
-
` : ''} - `; - viewContent.innerHTML = html; - JsBarcode("#@barcodeType", getBarcodeValue("@barcodeType", asset.Cn), { - format: "@barcodeType", - lineColor: "#000", - width: 2, - height: 80, - displayValue: true - }); - document.getElementById("downloadBtn").addEventListener("click", () => { - downloadBarcode("@barcodeType", getBarcodeValue("@barcodeType", asset.Cn)); - }); - document.getElementById("printBtn").addEventListener("click", () => { - addAssetIdToBatch(asset.Cn); - showToast("@T["Successfully added barcode to print batch"]", "success"); - bootstrap.Modal.getInstance('#viewAssetModal').hide(); - }); - } catch (err) { - console.error(err); - viewContent.innerHTML = `

@T["Error loading asset details"]

`; + 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'); + // Avoid clicks on buttons inside the row + if (e.target.closest('button')) return; + + const assetId = row.getAttribute('data-asset-id'); + viewContent.innerHTML = '

@T["Loading..."]

'; + + const modal = new bootstrap.Modal(viewModal); + modal.show(); + + try { + const response = await fetch(`/Assets/Get?cn=${assetId}`); + const json = await response.json(); + const asset = json.assetsModel; + + if (!asset) { + viewContent.innerHTML = `

@T["Asset not found."]

`; + return; + } + let i = 0; + const html = ` +
+

@T["Barcode"]

+
+ +
+
+ + +
+
+
+
+

@T["Inventory"]

+
+ + +
+
+ + +
+
+
+
+

@T["Information"]

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

@T["Description"]

+
+ + +
+
+ + +
+
+ + +
+
+ ${asset.Description?.Attributes ? ` +
+
+

@T["Attributes"]

+ ${Object.entries(asset.Description.Attributes) + .map(([k,v]) => ` +
+ : + +
`) + .join('')} +
` : ''} + + ${asset.Description?.Purchase ? ` +
+
+

@T["Purchase Information"]

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
` : ''} + `; + viewContent.innerHTML = html; + JsBarcode("#@barcodeType", getBarcodeValue("@barcodeType", asset.Cn), { + format: "@barcodeType", + lineColor: "#000", + width: 2, + height: 80, + displayValue: true + }); + document.getElementById("downloadBtn").addEventListener("click", () => { + downloadBarcode("@barcodeType", getBarcodeValue("@barcodeType", asset.Cn)); + }); + document.getElementById("printBtn").addEventListener("click", () => { + addAssetIdToBatch(asset.Cn); + showToast("@T["Successfully added barcode to print batch"]", "success"); + bootstrap.Modal.getInstance('#viewAssetModal').hide(); + }); + } catch (err) { + console.error(err); + viewContent.innerHTML = `

@T["Error loading asset details"]

`; + } +} + document.addEventListener('DOMContentLoaded', () => { const presetApplyButton = document.getElementById('createPresetApply'); @@ -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 = ` - - - + + + + + + + + + `; attributesContainer.appendChild(row); } diff --git a/src/Views/Home/Groups.cshtml b/src/Views/Home/Groups.cshtml index aa765f6..34cda84 100644 --- a/src/Views/Home/Groups.cshtml +++ b/src/Views/Home/Groups.cshtml @@ -39,12 +39,12 @@ { @groupTableViewModel.Group - @(groupTableViewModel.CanInventorize ? "✓" : "✗") - @(groupTableViewModel.CanManageUsers ? "✓" : "✗") - @(groupTableViewModel.CanManageLocations ? "✓" : "✗") - @(groupTableViewModel.CanManageAssets ? "✓" : "✗") - @(groupTableViewModel.CanManageGroups ? "✓" : "✗") - @(groupTableViewModel.CanManageSettings ? "✓" : "✗") + @T["inventorize"]@(groupTableViewModel.CanInventorize ? Html.Raw($"{@T["Yes"].Value}") : Html.Raw($"{@T["No"].Value}")) + @T["manage users"]@(groupTableViewModel.CanManageUsers ? Html.Raw($"{@T["Yes"].Value}") : Html.Raw($"{@T["No"].Value}")) + @T["manage locations"]@(groupTableViewModel.CanManageLocations ? Html.Raw($"{@T["Yes"].Value}") : Html.Raw($"{@T["No"].Value}")) + @T["manage assets"]@(groupTableViewModel.CanManageAssets ? Html.Raw($"{@T["Yes"].Value}") : Html.Raw($"{@T["No"].Value}")) + @T["manage groups"]@(groupTableViewModel.CanManageGroups ? Html.Raw($"{@T["Yes"].Value}") : Html.Raw($"{@T["No"].Value}")) + @T["manage settings"]@(groupTableViewModel.CanManageSettings ? Html.Raw($"{@T["Yes"].Value}") : Html.Raw($"{@T["No"].Value}"))