mirror of
https://github.com/LD-Reborn/Berufsschule_HAM.git
synced 2025-12-20 06:51:55 +00:00
Merge pull request #299 from LD-Reborn/256-feature-admin-settings-page---front-end
256 feature admin settings page front end
This commit is contained in:
@@ -33,7 +33,15 @@ public class SettingsController : Controller
|
||||
public async Task<IActionResult> AdminAsync()
|
||||
{
|
||||
AdminSettingsModel adminSettingsModel = await _ldap.GetAdminSettingsModelAsync();
|
||||
return View();
|
||||
return View(adminSettingsModel);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("Presets")]
|
||||
public async Task<Dictionary<string, Preset>> PresetsAsync()
|
||||
{
|
||||
AdminSettingsModel adminSettingsModel = await _ldap.GetAdminSettingsModelAsync();
|
||||
return adminSettingsModel.Presets;
|
||||
}
|
||||
|
||||
[Authorize(Roles = "CanManageSettings")]
|
||||
|
||||
@@ -178,4 +178,13 @@
|
||||
<data name="Date" xml:space="preserve">
|
||||
<value>Datum</value>
|
||||
</data>
|
||||
<data name="Preset" xml:space="preserve">
|
||||
<value>Vorlage</value>
|
||||
</data>
|
||||
<data name="Select preset" xml:space="preserve">
|
||||
<value>Vorlage auswählen</value>
|
||||
</data>
|
||||
<data name="Apply preset" xml:space="preserve">
|
||||
<value>Vorlage anwenden</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
91
src/Resources/Views.Settings.Admin.resx
Normal file
91
src/Resources/Views.Settings.Admin.resx
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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="Users" xml:space="preserve">
|
||||
<value>Benutzer</value>
|
||||
</data>
|
||||
|
||||
<data name="General settings" xml:space="preserve">
|
||||
<value>Allgemeine Einstellungen</value>
|
||||
</data>
|
||||
|
||||
<data name="Default hash algorithm" xml:space="preserve">
|
||||
<value>Standard-Hashalgorithmus</value>
|
||||
</data>
|
||||
|
||||
<data name="Barcode type" xml:space="preserve">
|
||||
<value>Barcode-Typ</value>
|
||||
</data>
|
||||
|
||||
<data name="Barcode text" xml:space="preserve">
|
||||
<value>Barcode-Text</value>
|
||||
</data>
|
||||
|
||||
<data name="Max downloadable user image size" xml:space="preserve">
|
||||
<value>Maximale Download-Größe für Benutzerbilder</value>
|
||||
</data>
|
||||
|
||||
<data name="Presets" xml:space="preserve">
|
||||
<value>Vorlagen</value>
|
||||
</data>
|
||||
|
||||
<data name="Preset name" xml:space="preserve">
|
||||
<value>Name der Vorlage</value>
|
||||
</data>
|
||||
|
||||
<data name="Attributes" xml:space="preserve">
|
||||
<value>Attribute</value>
|
||||
</data>
|
||||
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
|
||||
<data name="Value" xml:space="preserve">
|
||||
<value>Wert</value>
|
||||
</data>
|
||||
|
||||
<data name="Delete attribute" xml:space="preserve">
|
||||
<value>Attribut löschen</value>
|
||||
</data>
|
||||
|
||||
<data name="Add attribute" xml:space="preserve">
|
||||
<value>Attribut hinzufügen</value>
|
||||
</data>
|
||||
|
||||
<data name="Delete preset" xml:space="preserve">
|
||||
<value>Vorlage löschen</value>
|
||||
</data>
|
||||
|
||||
<data name="Add preset" xml:space="preserve">
|
||||
<value>Vorlage hinzufügen</value>
|
||||
</data>
|
||||
|
||||
<data name="Apply settings and update presets" xml:space="preserve">
|
||||
<value>Einstellungen übernehmen und Vorlagen aktualisieren</value>
|
||||
</data>
|
||||
|
||||
<data name="Settings updated successfully" xml:space="preserve">
|
||||
<value>Einstellungen erfolgreich aktualisiert</value>
|
||||
</data>
|
||||
|
||||
<data name="Unknown error" xml:space="preserve">
|
||||
<value>Unbekannter Fehler</value>
|
||||
</data>
|
||||
|
||||
<data name="Error contacting server" xml:space="preserve">
|
||||
<value>Fehler bei der Kommunikation mit dem Server</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -43,7 +43,13 @@
|
||||
<data name="Error loading locations" xml:space="preserve">
|
||||
<value>Fehler beim Laden der Orte</value>
|
||||
</data>
|
||||
<data name="Error loading presets" xml:space="preserve">
|
||||
<value>Fehler beim Laden der Vorlagen</value>
|
||||
</data>
|
||||
<data name="Select user" xml:space="preserve">
|
||||
<value>Benutzer auswählen</value>
|
||||
</data>
|
||||
<data name="Select preset" xml:space="preserve">
|
||||
<value>Vorlage auswählen</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
@using Berufsschule_HAM.Services
|
||||
@using Microsoft.AspNetCore.Mvc.Localization
|
||||
@using Berufsschule_HAM.Models
|
||||
@model HomeIndexViewModel
|
||||
@inject IViewLocalizer T
|
||||
@inject IConfiguration Configuration
|
||||
@inject LdapService ldap
|
||||
@{
|
||||
ViewData["Title"] = T["Assets"];
|
||||
string barcodeType = Configuration["BarcodeType"] ?? "EAN13";
|
||||
var adminSettingsModel = await ldap.GetAdminSettingsModelAsync();
|
||||
string barcodeType = adminSettingsModel.BarcodeType ?? "EAN13";
|
||||
string barcodeText = adminSettingsModel.BarcodeText ?? "HAM";
|
||||
}
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select/dist/css/tom-select.bootstrap5.min.css" rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select/dist/js/tom-select.complete.min.js" defer></script>
|
||||
@@ -192,6 +195,18 @@
|
||||
|
||||
<hr class="my-3" />
|
||||
|
||||
<h4 class="fw-bold">@T["Preset"]</h4>
|
||||
<div class="col-md-8">
|
||||
<select class="form-select" name="Preset" aria-label="@T["Preset"]" id="createPresetSelect">
|
||||
<option value="">@T["Select preset"]</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="button" id="createPresetApply" class="btn btn-sm btn-warning">@T["Apply preset"]</button>
|
||||
</div>
|
||||
|
||||
<hr class="my-3" />
|
||||
|
||||
<!-- Description Section -->
|
||||
<h4 class="fw-bold">@T["Description"]</h4>
|
||||
<div class="col-md-6">
|
||||
@@ -254,7 +269,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Open modal if URL contains parameter: CreateModal=true
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -803,11 +818,77 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
createModal.addEventListener('show.bs.modal', async () => {
|
||||
const selectLocations = createModal.querySelector('#createLocationSelect');
|
||||
await loadLocationsIntoSelect(selectLocations);
|
||||
const selectPresets = createModal.querySelector('#createPresetSelect');
|
||||
await loadPresetsIntoSelect(selectPresets);
|
||||
const selectUsers = createModal.querySelector('#createUsersSelect');
|
||||
await loadUsersIntoSelect(selectUsers);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const presetApplyButton = document.getElementById('createPresetApply');
|
||||
presetApplyButton.addEventListener('click', async () => {
|
||||
const response = await fetch('/Settings/Presets');
|
||||
const responseJson = await response.json();
|
||||
const presetSelect = document.getElementById('createPresetSelect');
|
||||
const presetSelectValue = presetSelect.value;
|
||||
if (responseJson[presetSelectValue]) {
|
||||
const attributesContainer = document.getElementById('attributesContainer');
|
||||
var attributes = responseJson[presetSelectValue].attribute;
|
||||
for (var attributeKey in attributes) {
|
||||
var attributeValue = attributes[attributeKey];
|
||||
if (attributeKey == "Type" || attributeKey == "@T["Type"]") {
|
||||
document.getElementById("createType").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Make" || attributeKey == "@T["Make"]") {
|
||||
document.getElementById("createMake").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Model" || attributeKey == "@T["Model"]") {
|
||||
document.getElementById("createModel").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Purchase Date" || attributeKey == "@T["Purchase Date"]") {
|
||||
document.getElementById("createPurchaseDate").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Purchase Value" || attributeKey == "@T["Purchase Value"]") {
|
||||
document.getElementById("createPurchaseValue").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Purchased At" || attributeKey == "@T["Purchased At"]") {
|
||||
document.getElementById("createPurchaseAt").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Purchased By" || attributeKey == "@T["Purchased By"]") {
|
||||
document.getElementById("createPurchaseBy").setAttribute("value", attributeValue);
|
||||
} else if (attributeKey == "Location" || attributeKey == "@T["Location"]") {
|
||||
document.getElementById("createLocationSelect-ts-control").value = attributeValue;
|
||||
document.getElementById("createLocationSelect").value = attributeValue;
|
||||
} else if (attributeKey == "User" || attributeKey == "@T["User"]") {
|
||||
document.getElementById("createUsersSelect-ts-control").value = attributeValue;
|
||||
document.getElementById("createUsersSelect").value = attributeValue;
|
||||
} else if (attributeKey == "Name" || attributeKey == "@T["Name"]") {
|
||||
document.getElementById("createName").value = attributeValue;
|
||||
} else if (attributeKey == "Serial Number" || attributeKey == "@T["Serial Number"]") {
|
||||
document.getElementById("createSerialNumber").value = attributeValue;
|
||||
} else {
|
||||
var nameInputs = attributesContainer.querySelectorAll("input[data-attr-name]");
|
||||
var anyEquals = false;
|
||||
var anyEqualsElement = null;
|
||||
nameInputs.forEach(element => {
|
||||
if (element.value == attributeKey) {
|
||||
anyEquals = true;
|
||||
anyEqualsElement = element.nextElementSibling;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (anyEquals) {
|
||||
anyEqualsElement.value = attributeValue;
|
||||
} else {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'd-flex gap-2 align-items-center attribute-row';
|
||||
row.innerHTML = `
|
||||
<input type="text" class="form-control" placeholder="@T["Attribute name"]" data-attr-name value="${attributeKey}" />
|
||||
<input type="text" class="form-control" placeholder="@T["Attribute value"]" data-attr-value value="${attributeValue}" />
|
||||
<button type="button" class="btn btn-danger btn-sm btn-remove-attribute">@T["Remove"]</button>
|
||||
`;
|
||||
attributesContainer.appendChild(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- TomSelect dropdowns -->
|
||||
<script>
|
||||
@@ -859,6 +940,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
initUsersSelect(createUsersSelect);
|
||||
initUsersSelect(updateUsersSelect);
|
||||
|
||||
const createPresetsSelect = document.getElementById('createPresetsSelect');
|
||||
|
||||
async function initPresetsSelect(selectElement) {
|
||||
if (!selectElement) return;
|
||||
await loadPresetsIntoSelect(selectElement);
|
||||
new TomSelect(selectElement, {
|
||||
plugins: ['clear_button'],
|
||||
create: false,
|
||||
sortField: { field: 'text', direction: 'asc' },
|
||||
placeholder: '@T["Select user"]',
|
||||
maxOptions: 500, // avoid performance hit if there are many
|
||||
render: {
|
||||
no_results: function(data, escape) {
|
||||
return `<div class="no-results">@T["No presets found"]</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initPresetsSelect(createPresetsSelect);
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -235,33 +235,6 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function unflatten(obj) {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const parts = key.split(".");
|
||||
let current = result;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (i === parts.length - 1) {
|
||||
if (typeof value === "string" && /^[\[{]/.test(value.trim())) {
|
||||
try {
|
||||
current[part] = JSON.parse(value);
|
||||
} catch {
|
||||
current[part] = value;
|
||||
}
|
||||
} else {
|
||||
current[part] = value;
|
||||
}
|
||||
} else {
|
||||
current[part] = current[part] || {};
|
||||
current = current[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- Update user modal ---
|
||||
const updateModal = document.getElementById('updateModal');
|
||||
|
||||
@@ -1 +1,351 @@
|
||||
<h3>Empty view lol</h3>
|
||||
@using Berufsschule_HAM.Services
|
||||
@using Microsoft.AspNetCore.Html
|
||||
@using Microsoft.AspNetCore.Mvc.Localization
|
||||
@using Berufsschule_HAM.Models
|
||||
@using System.Buffers.Text
|
||||
@using System.Text.Json;
|
||||
@using System.Text.Json.Serialization;
|
||||
@model AdminSettingsModel
|
||||
@inject IViewLocalizer T
|
||||
@inject LdapService ldap
|
||||
@{
|
||||
ViewData["Title"] = T["Users"];
|
||||
}
|
||||
|
||||
<form id="updateSettings" style="margin-bottom: 4rem !important" method="post" asp-controller="Settings" asp-action="Admin">
|
||||
<div class="row g-3">
|
||||
<h4 class="fw-bold">@T["General settings"]</h4>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="updateHashAlgorithm">@T["Default hash algorithm"]</label>
|
||||
<select type="text" name="DefaultHashAlgorithm" id="updateHashAlgorithm" class="form-control">
|
||||
@foreach (string algorithm in ldap.HashAlgorithms.Keys)
|
||||
{
|
||||
bool selected = algorithm == Model.DefaultHashAlgorithm;
|
||||
<option value="@algorithm" selected="@selected">@algorithm</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="updateBarcodeType">@T["Barcode type"]</label>
|
||||
<select type="text" name="BarcodeType" id="updateBarcodeType" class="form-control" />
|
||||
<option selected="@(Model.BarcodeType == "code128c")" value="code128c">CODE128C</option>
|
||||
<option selected="@(Model.BarcodeType == "ean13")" value="ean13">EAN13</option>
|
||||
<option selected="@(Model.BarcodeType == "ean8")" value="ean8">EAN8</option>
|
||||
<option selected="@(Model.BarcodeType == "upc")" value="upc">UPC</option>
|
||||
<option selected="@(Model.BarcodeType == "itf14")" value="itf14">ITF14</option>
|
||||
<option selected="@(Model.BarcodeType == "itf")" value="itf">ITF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="updateBarcodeText">@T["Barcode text"]</label>
|
||||
<input type="text" name="BarcodeText" id="updateBarcodeText" class="form-control" value="@Model.BarcodeText" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="updateMaxDownloadableUserImageSize">@T["Max downloadable user image size"]</label>
|
||||
<input type="number" id="updateMaxDownloadableUserImageSize" name="MaxDownloadableUserImageSize" class="form-control" value="@Model.MaxDownloadableUserImageSize" />
|
||||
</div>
|
||||
</div>
|
||||
<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;">
|
||||
|
||||
<h4 class="fw-bold m-0">@T["Presets"]</h4>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
viewBox="0 0 16 16" class="chevron-svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="row g-3 collapse" id="presetsCollapse">
|
||||
@foreach (var preset in Model.Presets)
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 mb-3" 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>
|
||||
@foreach (var attr in preset.Value.Attribute)
|
||||
{
|
||||
<div class="row mb-2" data-type="attribute">
|
||||
<div class="col-md-3 mt-2">
|
||||
<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">
|
||||
<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">
|
||||
<button type="button" class="btn btn-danger" data-type="deleteAttribute">@T["Delete attribute"]</button>
|
||||
</div>
|
||||
</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>
|
||||
<div class="col-md-4 ms-auto mt-2">
|
||||
<button type="button" class="btn btn-primary" data-type="addAttribute">@T["Add attribute"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="col-md-12 mt-2">
|
||||
<button type="button" class="btn btn-primary" data-type="addPreset">@T["Add preset"]</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning float-end mt-3">@T["Apply settings and update presets"]</button>
|
||||
</form>
|
||||
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const updateForm = document.getElementById('updateSettings');
|
||||
updateForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const url = `/Settings/Admin`;
|
||||
const dataFromEntries = Object.fromEntries(new FormData(updateForm).entries());
|
||||
var data = unflatten(dataFromEntries);
|
||||
var presets = {};
|
||||
for (var preset in data.Presets) {
|
||||
for (var attribute in data.Presets[preset].Attribute) {
|
||||
var key = data.Presets[preset].Attribute[attribute].Key;
|
||||
var value = data.Presets[preset].Attribute[attribute].Value;
|
||||
delete(data.Presets[preset].Attribute[attribute]);
|
||||
data.Presets[preset].Attribute[key] = value;
|
||||
}
|
||||
var presetKey = data.Presets[preset].Key;
|
||||
delete(data.Presets[preset].Key);
|
||||
var tempPreset = data.Presets[preset];
|
||||
delete(data.Presets[preset]);
|
||||
data.Presets[presetKey] = tempPreset;
|
||||
}
|
||||
var temp = data;
|
||||
data = {};
|
||||
data.AdminSettingsModel = temp;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.Success) {
|
||||
showToast('@T["Settings updated successfully"]', 'success');
|
||||
} else {
|
||||
showToast(`${result.reason}: ${result.exception || '@T["Unknown error"]'}`, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('@T["Error contacting server"]', 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function addAttribute(presetContainer, presetKey) {
|
||||
const attributeId = crypto.randomUUID();
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('row', 'mb-2');
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="col-md-3">
|
||||
<input class="form-control"
|
||||
name="Presets.${presetKey}.Attribute.${attributeId}.Key"
|
||||
value="" />
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control"
|
||||
name="Presets.${presetKey}.Attribute.${attributeId}.Value"
|
||||
value="" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-danger btn-delete-attr">
|
||||
${presetContainer.dataset.deleteAttributeText}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
presetContainer.querySelector('.attributes').appendChild(row);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(toggler => {
|
||||
toggler.addEventListener('click', async () => {
|
||||
const icon = toggler.querySelector('.chevron-svg');
|
||||
icon.classList.toggle('rotate');
|
||||
var target = document.querySelector(toggler.dataset.bsTarget);
|
||||
while (target.classList.contains('collapsing')) {
|
||||
await sleep(10);
|
||||
}
|
||||
icon.parentElement.classList.toggle("no-bottom-border");
|
||||
const collapseId = toggler.dataset.bsTarget.replace('#', '');
|
||||
const isCollapsed = target.classList.contains('show');
|
||||
// Update URL parameter
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set(collapseId, isCollapsed); // true = open, false = collapsed
|
||||
window.history.replaceState({}, '', url);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const collapseElements = document.querySelectorAll('.collapse');
|
||||
|
||||
collapseElements.forEach(el => {
|
||||
const paramValue = new URL(window.location).searchParams.get(el.id);
|
||||
if (paramValue === 'true') {
|
||||
el.classList.add('show');
|
||||
const toggler = document.querySelector(`[data-bs-target="#${el.id}"]`);
|
||||
if (toggler) {
|
||||
toggler.querySelector('.chevron-svg')?.classList.add('rotate');
|
||||
toggler.classList.add('no-bottom-border');
|
||||
}
|
||||
} else {
|
||||
el.classList.remove('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('button[data-type="addAttribute"]').forEach(btn => {
|
||||
addAddAttributeEventListener(btn);
|
||||
});
|
||||
|
||||
document.querySelectorAll('button[data-type="deleteAttribute"]').forEach(btn => {
|
||||
addRemoveAttributeEventListener(btn);
|
||||
});
|
||||
|
||||
document.querySelectorAll('button[data-type="deletePreset"]').forEach(btn => {
|
||||
addDeletePresetEventListener(btn);
|
||||
});
|
||||
|
||||
document.querySelectorAll('button[data-type="addPreset"]').forEach(btn => {
|
||||
addAddPresetEventListener(btn);
|
||||
});
|
||||
|
||||
function addAddAttributeEventListener(button) {
|
||||
button.addEventListener('click', e => {
|
||||
const presetContainer = e.target.closest('.border.rounded.p-3.mb-3');
|
||||
if (!presetContainer) return;
|
||||
|
||||
const presetKey = presetContainer.getAttribute("data-preset-id");
|
||||
if (!presetKey) return;
|
||||
|
||||
// Container für Attribute suchen
|
||||
let attributesContainer = presetContainer.querySelector('.attributes');
|
||||
if (!attributesContainer) {
|
||||
// Falls nicht vorhanden -> dynamisch anlegen (optional)
|
||||
attributesContainer = document.createElement('div');
|
||||
attributesContainer.classList.add('attributes');
|
||||
presetContainer.insertBefore(attributesContainer, presetContainer.querySelector('.row.mb-4'));
|
||||
}
|
||||
|
||||
const attributeId = crypto.randomUUID();
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('row', 'mb-2');
|
||||
row.setAttribute('data-type', 'attribute');
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="col-md-3">
|
||||
<input class="form-control"
|
||||
name="Presets.${presetKey}.Attribute.${attributeId}.Key"
|
||||
value="" />
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input class="form-control"
|
||||
name="Presets.${presetKey}.Attribute.${attributeId}.Value"
|
||||
value="" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-danger" data-type="deleteAttribute">
|
||||
${presetContainer.dataset.deleteAttributeText || 'Delete attribute'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
attributesContainer.appendChild(row);
|
||||
|
||||
// Delete-Button Eventlistener aktivieren
|
||||
const deleteButton = row.querySelector('button[data-type="deleteAttribute"]');
|
||||
addRemoveAttributeEventListener(deleteButton);
|
||||
});
|
||||
}
|
||||
|
||||
function addRemoveAttributeEventListener(button) {
|
||||
button.addEventListener('click', e => {
|
||||
const row = e.target.closest('div[data-type="attribute"]');
|
||||
row.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function addDeletePresetEventListener(button) {
|
||||
button.addEventListener('click', e => {
|
||||
const presetContainer = e.target.closest('.col-md-6');
|
||||
if (!presetContainer) return;
|
||||
presetContainer.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function addAddPresetEventListener(button) {
|
||||
button.addEventListener('click', e => {
|
||||
// Presets-Container: der nächste Block über dem Button, der die Presets enthält
|
||||
const presetsButton = button.closest('button[data-type="addPreset"]');
|
||||
if (!presetsButton) return;
|
||||
|
||||
const presetId = crypto.randomUUID();
|
||||
|
||||
// Preset-Block erstellen
|
||||
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}">
|
||||
<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>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-danger" data-type="deletePreset">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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Einfach ans Ende der Container-Liste hängen
|
||||
presetsButton.parentElement.parentElement.insertBefore(presetDiv, presetsButton.parentElement);
|
||||
|
||||
// Eventlistener für den neuen Preset-Block aktivieren
|
||||
const addAttrBtn = presetDiv.querySelector('button[data-type="addAttribute"]');
|
||||
const deletePresetBtn = presetDiv.querySelector('button[data-type="deletePreset"]');
|
||||
|
||||
addAddAttributeEventListener(addAttrBtn);
|
||||
addDeletePresetEventListener(deletePresetBtn);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style defer>
|
||||
.chevron-svg {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.chevron-svg.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.no-bottom-border {
|
||||
border-bottom: none !important;
|
||||
border-radius: 0.5rem 0.5rem 0 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -25,7 +25,9 @@
|
||||
selectLocation: '@T["Select location"]',
|
||||
errorLoadingLocations: '@T["Error loading locations"]',
|
||||
selectUser: '@T["Select user"]',
|
||||
errorLoadingUsers: '@T["Error loading users"]'
|
||||
selectPreset: '@T["Select preset"]',
|
||||
errorLoadingUsers: '@T["Error loading users"]',
|
||||
errorLoadingPresets: '@T["Error loading presets"]'
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -239,4 +239,66 @@ function validatePassword(password) {
|
||||
const strongPasswordRegex =
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_\-+=\[{\]};:'",<.>/?\\|`~]).{8,}$/;
|
||||
return strongPasswordRegex.test(password);
|
||||
}
|
||||
|
||||
function unflatten(obj) {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const parts = key.split(".");
|
||||
let current = result;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (i === parts.length - 1) {
|
||||
if (typeof value === "string" && /^[\[{]/.test(value.trim())) {
|
||||
try {
|
||||
current[part] = JSON.parse(value);
|
||||
} catch {
|
||||
current[part] = value;
|
||||
}
|
||||
} else {
|
||||
current[part] = value;
|
||||
}
|
||||
} else {
|
||||
current[part] = current[part] || {};
|
||||
current = current[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function loadPresetsIntoSelect(selectElement, selectedValue = null) {
|
||||
try {
|
||||
const response = await fetch('/Settings/Presets');
|
||||
const responseJson = await response.json();
|
||||
var presets = [];
|
||||
for (var key in responseJson) {
|
||||
presets.push(key);
|
||||
}
|
||||
console.log(presets);
|
||||
const ts = selectElement.tomselect;
|
||||
if (!ts) {
|
||||
selectElement.innerHTML = `<option value="">${appTranslations.selectPreset}</option>`;
|
||||
presets.forEach(u => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = u;
|
||||
opt.textContent = u;
|
||||
selectElement.appendChild(opt);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ts.clearOptions();
|
||||
ts.addOption(presets.map(u => ({ value: u, text: u })));
|
||||
ts.refreshOptions(false);
|
||||
|
||||
if (selectedValue) ts.setValue(selectedValue);
|
||||
} catch (err) {
|
||||
console.error('Error loading presets:', err);
|
||||
showToast(appTranslations.errorLoadingPresets, 'danger');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user