mirror of
https://github.com/LD-Reborn/Berufsschule_HAM.git
synced 2025-12-20 06:51:55 +00:00
Merge pull request #168 from LD-Reborn/156-feature-add-users-front-end-crud
156 feature add users front end crud
This commit is contained in:
@@ -89,7 +89,7 @@ public class HomeController : Controller
|
||||
Surname = user.Sn ?? "",
|
||||
Title = user.Title ?? "",
|
||||
Uid = user.Uid,
|
||||
Workplace = user.Description?.Workplace ?? ""
|
||||
Description = user.Description ?? new() {Address = new(), BirthDate = "", Workplace = "", Groups = []}
|
||||
});
|
||||
}
|
||||
return View(new UsersIndexViewModel() { UserTableViewModels = UserTableViewModels });
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Controllers/UsersController.cs
|
||||
using Berufsschule_HAM.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Novell.Directory.Ldap;
|
||||
@@ -8,6 +7,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Text.Json;
|
||||
using System.Buffers.Text;
|
||||
|
||||
[Authorize(Roles = "CanManageUsers")]
|
||||
[Route("[controller]")]
|
||||
@@ -60,15 +60,19 @@ public class UsersController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("Create")]
|
||||
public async Task<bool> Create(string cn, string sn, string? title, string? uid, string userPassword, string? description, string jpegPhoto)
|
||||
[HttpPost("Create")]
|
||||
public async Task<UsersCreateResponseModel> Create([FromBody] UsersCreateRequestModel requestModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? jpegPhoto = requestModel.JpegPhoto;
|
||||
string? title = requestModel.Title;
|
||||
string userPassword = requestModel.UserPassword ?? "";
|
||||
UserDescription? description = requestModel.Description;
|
||||
jpegPhoto ??= Convert.ToBase64String(System.IO.File.ReadAllBytes("wwwroot/user_default.jpeg")); // TODO: cleanup - make this a config setting
|
||||
uid ??= sn.ToLower() + cn.ToLower();
|
||||
string uid = UsersHelper.CreateUsername(requestModel.Cn ?? "", requestModel.Sn ?? "");
|
||||
title ??= "";
|
||||
description ??= "{}";
|
||||
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
|
||||
if (!userPassword.StartsWith('{'))
|
||||
{
|
||||
byte[] passwordBytes = Encoding.UTF8.GetBytes(userPassword);
|
||||
@@ -79,51 +83,37 @@ public class UsersController : Controller
|
||||
LdapAttributeSet attributeSet =
|
||||
[
|
||||
new LdapAttribute("objectClass", "inetOrgPerson"),
|
||||
new LdapAttribute("cn", cn),
|
||||
new LdapAttribute("sn", sn),
|
||||
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", description),
|
||||
new LdapAttribute("description", JsonSerializer.Serialize(description)),
|
||||
new LdapAttribute("userPassword", userPassword),
|
||||
];
|
||||
await _ldap.CreateUser(uid, attributeSet);
|
||||
return true;
|
||||
return new(){Success = true, Uid = uid};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to create user: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]);
|
||||
return false;
|
||||
return new() {Success = false, Exception = ex.Message};
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Update")]
|
||||
public async Task<bool> Update([FromBody] UsersModifyRequestModel requestModel)
|
||||
public async Task<UsersUpdateResponseModel> Update([FromBody] UsersModifyRequestModel requestModel)
|
||||
{
|
||||
if (requestModel is null)
|
||||
{
|
||||
_logger.LogError("Unable to update a user because the UsersModifyRequestModel is null");
|
||||
return false;
|
||||
return new() { Success = false };
|
||||
}
|
||||
string uid = requestModel.uid;
|
||||
try
|
||||
{
|
||||
string uid = requestModel.Uid;
|
||||
UserModel? user = null;
|
||||
if (requestModel.Cn is not null)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "cn", requestModel.Cn);
|
||||
user ??= await _ldap.GetUserByUidAsync(uid);
|
||||
string newUid = user.Sn?.ToLower() + requestModel.Cn.ToLower();
|
||||
await _ldap.UpdateUser(uid, "uid", newUid);
|
||||
uid = newUid;
|
||||
}
|
||||
if (requestModel.Sn is not null)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "sn", requestModel.Sn);
|
||||
user ??= await _ldap.GetUserByUidAsync(uid);
|
||||
string newUid = requestModel.Sn.ToLower() + user.Cn?.ToLower();
|
||||
await _ldap.UpdateUser(uid, "uid", newUid);
|
||||
uid = newUid;
|
||||
}
|
||||
if (requestModel.NewUid is not null)
|
||||
if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "uid", requestModel.NewUid);
|
||||
uid = requestModel.NewUid;
|
||||
@@ -134,17 +124,45 @@ public class UsersController : Controller
|
||||
}
|
||||
if (requestModel.Description is not null)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "description", requestModel.Description);
|
||||
await _ldap.UpdateUser(uid, "description", JsonSerializer.Serialize(requestModel.Description));
|
||||
}
|
||||
if (requestModel.JpegPhoto is not null)
|
||||
if (requestModel.JpegPhoto is not null && requestModel.JpegPhoto.Length > 0)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "jpegPhoto", requestModel.JpegPhoto);
|
||||
}
|
||||
if (requestModel.UserPassword is not null)
|
||||
if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword);
|
||||
await _ldap.UpdateUser(uid, "userPassword", "{SHA256}" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(requestModel.UserPassword))));
|
||||
}
|
||||
|
||||
string newUid = uid;
|
||||
if (requestModel.Cn is not null)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "cn", requestModel.Cn);
|
||||
user ??= await _ldap.GetUserByUidAsync(uid);
|
||||
newUid = UsersHelper.CreateUsername(requestModel.Cn, user.Sn ?? "");
|
||||
|
||||
}
|
||||
if (requestModel.Sn is not null)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "sn", requestModel.Sn);
|
||||
user ??= await _ldap.GetUserByUidAsync(uid);
|
||||
newUid = UsersHelper.CreateUsername(user.Cn ?? "", requestModel.Sn);
|
||||
}
|
||||
if (newUid.Length == 0)
|
||||
{
|
||||
throw new Exception("Username cannot be empty");
|
||||
}
|
||||
if (newUid != uid)
|
||||
{
|
||||
await _ldap.UpdateUser(uid, "uid", newUid);
|
||||
uid = newUid;
|
||||
}
|
||||
return new() { Success = true, NewUid = uid };
|
||||
} catch (Exception ex)
|
||||
{
|
||||
return new() { Success = false, Exception = ex.Message };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[HttpPost("AddGroup")]
|
||||
|
||||
44
src/Helpers/UsersHelper.cs
Normal file
44
src/Helpers/UsersHelper.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
namespace Berufsschule_HAM.Helpers;
|
||||
|
||||
public static partial class UsersHelper
|
||||
{
|
||||
public static string CreateUsername(string givenName, string surname)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(givenName) || string.IsNullOrWhiteSpace(surname))
|
||||
throw new ArgumentException("Given name and surname must not be empty.");
|
||||
|
||||
string combined = (surname + givenName).ToLowerInvariant();
|
||||
|
||||
// Normalize to decompose accents (e.g., é -> e + ́)
|
||||
string normalized = combined.Normalize(NormalizationForm.FormD);
|
||||
|
||||
// Remove diacritics
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in normalized)
|
||||
{
|
||||
var category = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
if (category != UnicodeCategory.NonSpacingMark)
|
||||
sb.Append(c);
|
||||
}
|
||||
|
||||
string cleaned = sb.ToString();
|
||||
|
||||
// Replace German ligatures etc.
|
||||
cleaned = cleaned
|
||||
.Replace("ß", "ss")
|
||||
.Replace("æ", "ae")
|
||||
.Replace("œ", "oe")
|
||||
.Replace("ø", "o");
|
||||
|
||||
// Remove everything not a-z
|
||||
cleaned = AtoZ().Replace(cleaned, "");
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
[GeneratedRegex("[^a-z]")]
|
||||
private static partial Regex AtoZ();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Berufsschule_HAM.Models;
|
||||
|
||||
@@ -26,17 +27,24 @@ public class UserModel
|
||||
|
||||
public class UserDescription
|
||||
{
|
||||
[JsonPropertyName("BirthDate")]
|
||||
public required string BirthDate { get; set; }
|
||||
[JsonPropertyName("Address")]
|
||||
public required UserAddress Address { get; set; }
|
||||
[JsonPropertyName("Workplace")]
|
||||
public required string Workplace { get; set; }
|
||||
[JsonPropertyName("Groups")]
|
||||
public List<string>? Groups { get; set; }
|
||||
}
|
||||
|
||||
public class UserAddress
|
||||
{
|
||||
[JsonPropertyName("City")]
|
||||
public string? City { get; set; }
|
||||
[JsonPropertyName("Street")]
|
||||
public string? Street { get; set; }
|
||||
public string? StreetNr { get; set; }
|
||||
[JsonPropertyName("StreetNr")]
|
||||
public int? StreetNr { get; set; }
|
||||
}
|
||||
|
||||
public class UserAuthenticationResult
|
||||
@@ -61,5 +69,5 @@ public class UserTableViewModel
|
||||
public required string Title { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Surname { get; set; }
|
||||
public required string Workplace { get; set; }
|
||||
public required UserDescription Description { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Berufsschule_HAM.Models;
|
||||
|
||||
public class UsersIndexRequestModel
|
||||
@@ -11,15 +13,27 @@ public class UsersIndexRequestModel
|
||||
public bool UserPassword { get; set; } = true;
|
||||
}
|
||||
|
||||
public class UsersModifyRequestModel
|
||||
public class UsersModifyRequestModel : UsersCreateRequestModel
|
||||
{
|
||||
public required string uid { get; set; }
|
||||
[JsonPropertyName("Uid")]
|
||||
public required string Uid { get; set; }
|
||||
[JsonPropertyName("NewUid")]
|
||||
public string? NewUid { get; set; } = null;
|
||||
}
|
||||
|
||||
public class UsersCreateRequestModel
|
||||
{
|
||||
[JsonPropertyName("Cn")]
|
||||
public string? Cn { get; set; } = null;
|
||||
[JsonPropertyName("Sn")]
|
||||
public string? Sn { get; set; } = null;
|
||||
[JsonPropertyName("Title")]
|
||||
public string? Title { get; set; } = null;
|
||||
public string? Description { get; set; } = null;
|
||||
[JsonPropertyName("Description")]
|
||||
public UserDescription? Description { get; set; } = null;
|
||||
[JsonPropertyName("JpegPhoto")]
|
||||
public string? JpegPhoto { get; set; } = null;
|
||||
[JsonPropertyName("UserPassword")]
|
||||
public string? UserPassword { get; set; } = null;
|
||||
}
|
||||
|
||||
|
||||
24
src/Models/UsersResponseModels.cs
Normal file
24
src/Models/UsersResponseModels.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Berufsschule_HAM.Models;
|
||||
|
||||
public class UsersUpdateResponseModel
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
[JsonPropertyName("Exception")]
|
||||
public string? Exception { get; set; }
|
||||
[JsonPropertyName("NewUid")]
|
||||
public string? NewUid { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class UsersCreateResponseModel
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
[JsonPropertyName("Exception")]
|
||||
public string? Exception { get; set; }
|
||||
[JsonPropertyName("Uid")]
|
||||
public string? Uid { get; set; }
|
||||
}
|
||||
@@ -19,4 +19,112 @@
|
||||
<data name="Users" xml:space="preserve">
|
||||
<value>Benutzer</value>
|
||||
</data>
|
||||
<data name="User deleted successfully" xml:space="preserve">
|
||||
<value>Benutzer erfolgreich gelöscht</value>
|
||||
</data>
|
||||
<data name="Unknown error" xml:space="preserve">
|
||||
<value>Unbekannter Fehler</value>
|
||||
</data>
|
||||
<data name="Error contacting server" xml:space="preserve">
|
||||
<value>Server konnte nicht erreicht werden</value>
|
||||
</data>
|
||||
<data name="User updated successfully" xml:space="preserve">
|
||||
<value>Benutzer wurde erfolgreich angepasst</value>
|
||||
</data>
|
||||
<data name="Update failed" xml:space="preserve">
|
||||
<value>Anpassung ist fehlgeschlagen</value>
|
||||
</data>
|
||||
<data name="Error updating user" xml:space="preserve">
|
||||
<value>Fehler beim Anpassen des Benutzers</value>
|
||||
</data>
|
||||
<data name="User created successfully" xml:space="preserve">
|
||||
<value>Benutzer wurde erfolgreich erstellt</value>
|
||||
</data>
|
||||
<data name="Create failed" xml:space="preserve">
|
||||
<value>Erstellung des Benutzers ist fehlgeschlagen</value>
|
||||
</data>
|
||||
<data name="Error creating user" xml:space="preserve">
|
||||
<value>Fehler beim Erstellen des Benutzers</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>Benutzername</value>
|
||||
</data>
|
||||
<data name="Title" xml:space="preserve">
|
||||
<value>Anrede</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="Surname" xml:space="preserve">
|
||||
<value>Nachname</value>
|
||||
</data>
|
||||
<data name="Workplace" xml:space="preserve">
|
||||
<value>Arbeitsplatz</value>
|
||||
</data>
|
||||
<data name="Action" xml:space="preserve">
|
||||
<value>Aktion</value>
|
||||
</data>
|
||||
<data name="Update" xml:space="preserve">
|
||||
<value>Anpassen</value>
|
||||
</data>
|
||||
<data name="Delete" xml:space="preserve">
|
||||
<value>Löschen</value>
|
||||
</data>
|
||||
<data name="DeleteUserConfirmation" xml:space="preserve">
|
||||
<value>Sind Sie sich sicher, dass Sie den Benutzer {0} löschen möchten?</value>
|
||||
</data>
|
||||
<data name="Yes, Delete" xml:space="preserve">
|
||||
<value>Ja, löschen</value>
|
||||
</data>
|
||||
<data name="Cancel" xml:space="preserve">
|
||||
<value>Abbrechen</value>
|
||||
</data>
|
||||
<data name="Update User" xml:space="preserve">
|
||||
<value>Benutzer anpassen</value>
|
||||
</data>
|
||||
<data name="Personal information" xml:space="preserve">
|
||||
<value>Persönliche Informationen</value>
|
||||
</data>
|
||||
<data name="Birth Date" xml:space="preserve">
|
||||
<value>Geburtsdatum</value>
|
||||
</data>
|
||||
<data name="City" xml:space="preserve">
|
||||
<value>Stadt</value>
|
||||
</data>
|
||||
<data name="Street" xml:space="preserve">
|
||||
<value>Straße</value>
|
||||
</data>
|
||||
<data name="Street Nr." xml:space="preserve">
|
||||
<value>Hausnummer</value>
|
||||
</data>
|
||||
<data name="Workplace & account" xml:space="preserve">
|
||||
<value>Arbeitsplatz und Benutzerkonto</value>
|
||||
</data>
|
||||
<data name="Groups" xml:space="preserve">
|
||||
<value>Gruppen</value>
|
||||
</data>
|
||||
<data name="New Password" xml:space="preserve">
|
||||
<value>Neues Passwort</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>Passwort</value>
|
||||
</data>
|
||||
<data name="Photo" xml:space="preserve">
|
||||
<value>Foto</value>
|
||||
</data>
|
||||
<data name="Save changes" xml:space="preserve">
|
||||
<value>Änderungen anwenden</value>
|
||||
</data>
|
||||
<data name="Create" xml:space="preserve">
|
||||
<value>Anlegen</value>
|
||||
</data>
|
||||
<data name="User Details" xml:space="preserve">
|
||||
<value>Benutzerdaten</value>
|
||||
</data>
|
||||
<data name="Address" xml:space="preserve">
|
||||
<value>Adresse</value>
|
||||
</data>
|
||||
<data name="Confirm Delete" xml:space="preserve">
|
||||
<value>Löschen bestätigen</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
19
src/Resources/Views.Home.Users.en.resx
Normal file
19
src/Resources/Views.Home.Users.en.resx
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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="DeleteUserConfirmation" xml:space="preserve">
|
||||
<value>Are you sure you want to delete user {0}?</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,6 +1,9 @@
|
||||
@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 UsersIndexViewModel
|
||||
@inject IViewLocalizer T
|
||||
@{
|
||||
@@ -13,7 +16,9 @@
|
||||
|
||||
|
||||
<div class="mb-4 d-flex flex-wrap gap-2">
|
||||
<button class="btn btn-primary">@T["Create user"]</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
|
||||
@T["Create user"]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,19 +27,19 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2rem;"></th>
|
||||
<th>User ID</th>
|
||||
<th>title</th>
|
||||
<th>Name</th>
|
||||
<th>Surname</th>
|
||||
<th>Workplace</th>
|
||||
<th>Action</th>
|
||||
<th>@T["Username"]</th>
|
||||
<th>@T["Title"]</th>
|
||||
<th>@T["Name"]</th>
|
||||
<th>@T["Surname"]</th>
|
||||
<th>@T["Workplace"]</th>
|
||||
<th>@T["Action"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{
|
||||
foreach (UserTableViewModel userTableViewModel in Model.UserTableViewModels)
|
||||
{
|
||||
<tr>
|
||||
<tr class="user-row">
|
||||
<td>
|
||||
<img class="rounded-circle user-icon" src="data:image/jpeg;base64,@userTableViewModel.JpegPhoto" alt="Photo" style="max-width:300px;" />
|
||||
</td>
|
||||
@@ -42,15 +47,29 @@
|
||||
<td>@userTableViewModel.Title</td>
|
||||
<td>@userTableViewModel.Name</td>
|
||||
<td>@userTableViewModel.Surname</td>
|
||||
<td>@userTableViewModel.Workplace</td>
|
||||
<td>@userTableViewModel.Description.Workplace</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-primary">Update</button>
|
||||
<button class="btn btn-sm btn-warning"
|
||||
data-user-id="@userTableViewModel.Uid"
|
||||
data-user-title="@userTableViewModel.Title"
|
||||
data-user-name="@userTableViewModel.Name"
|
||||
data-user-surname="@userTableViewModel.Surname"
|
||||
data-user-workplace="@userTableViewModel.Description.Workplace"
|
||||
data-user-birthdate="@userTableViewModel.Description.BirthDate"
|
||||
data-user-address-city="@userTableViewModel.Description.Address.City"
|
||||
data-user-address-street="@userTableViewModel.Description.Address.Street"
|
||||
data-user-address-streetnr="@userTableViewModel.Description.Address.StreetNr"
|
||||
data-user-groups="@JsonSerializer.Serialize(userTableViewModel.Description.Groups)"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#updateModal">
|
||||
@T["Update"]
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete"
|
||||
data-user-id="@userTableViewModel.Uid"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
🗑️ Delete
|
||||
@T["Delete"]
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -67,16 +86,18 @@
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
|
||||
<h5 class="modal-title" id="deleteModalLabel">@T["Confirm Delete"]</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete the user <strong id="userName"></strong> (ID: <span id="userId"></span>)?</p>
|
||||
<p>
|
||||
@T["DeleteUserConfirmation", new HtmlString("<strong id=\"userId\"></strong>")]
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form id="deleteForm" method="post" action="">
|
||||
<button type="submit" class="btn btn-danger">Yes, Delete</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@T["Cancel"]</button>
|
||||
<form id="deleteForm" method="post" asp-controller="Users" asp-action="Delete">
|
||||
<button type="submit" class="btn btn-danger">@T["Yes, Delete"]</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,62 +106,6 @@
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
let currentButton = null; // The delete button that opened the modal
|
||||
|
||||
deleteModal.addEventListener('show.bs.modal', event => {
|
||||
currentButton = event.relatedTarget; // Button that triggered the modal
|
||||
const userId = currentButton.getAttribute('data-user-id');
|
||||
const userName = currentButton.getAttribute('data-user-name');
|
||||
|
||||
deleteModal.querySelector('#userId').textContent = userId;
|
||||
deleteModal.querySelector('#userName').textContent = userName;
|
||||
|
||||
// Store the delete URL for later use
|
||||
deleteModal.querySelector('#deleteForm').dataset.url = `/Users/Delete?uid=${userId}`;
|
||||
});
|
||||
|
||||
// Handle submit of deleteForm via fetch()
|
||||
const deleteForm = document.getElementById('deleteForm');
|
||||
deleteForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = deleteForm.dataset.url;
|
||||
const userId = deleteModal.querySelector('#userId').textContent;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}//,
|
||||
//body: JSON.stringify({ id: userId }) // Use this for Post requests with [FromBody] parameters like in /Groups/Update
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Close the modal
|
||||
const modal = bootstrap.Modal.getInstance(deleteModal);
|
||||
modal.hide();
|
||||
|
||||
// Remove the row from the table
|
||||
const row = currentButton.closest('tr');
|
||||
row.classList.add('table-danger');
|
||||
setTimeout(() => row.remove(), 300);
|
||||
|
||||
showToast('User deleted successfully', 'success');
|
||||
} else {
|
||||
showToast(`❌ ${result.reason}: ${result.exception || 'Unknown error'}`, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Error contacting server', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// Simple toast helper
|
||||
function showToast(message, type) {
|
||||
const toastContainer = document.getElementById('toastContainer') || createToastContainer();
|
||||
@@ -166,5 +131,655 @@
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
let currentButton = null; // The delete button that opened the modal
|
||||
|
||||
deleteModal.addEventListener('show.bs.modal', event => {
|
||||
currentButton = event.relatedTarget; // Button that triggered the modal
|
||||
const userId = currentButton.getAttribute('data-user-id');
|
||||
const userName = currentButton.getAttribute('data-user-name');
|
||||
|
||||
deleteModal.querySelector('#userId').textContent = userId;
|
||||
deleteModal.querySelector('#userName').textContent = userName;
|
||||
|
||||
});
|
||||
|
||||
// Handle submit of deleteForm via fetch()
|
||||
const deleteForm = document.getElementById('deleteForm');
|
||||
deleteForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
console.log(deleteForm);
|
||||
const userId = deleteModal.querySelector('#userId').textContent;
|
||||
const url = `/Users/Delete?uid=${userId}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Close the modal
|
||||
const modal = bootstrap.Modal.getInstance(deleteModal);
|
||||
modal.hide();
|
||||
|
||||
// Remove the row from the table
|
||||
const row = currentButton.closest('tr');
|
||||
row.classList.add('table-danger');
|
||||
setTimeout(() => row.remove(), 300);
|
||||
|
||||
showToast('@T["User deleted successfully"]', 'success');
|
||||
} else {
|
||||
showToast(`${result.reason}: ${result.exception || '@T["Unknown error"]'}`, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('@T["Error contacting server"]', 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- User Update Modal -->
|
||||
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning text-dark">
|
||||
<h5 class="modal-title" id="updateModalLabel">@T["Update User"]</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="updateForm">
|
||||
<input type="hidden" id="updateUid" name="Uid" />
|
||||
|
||||
<div class="row g-3">
|
||||
<h6 class="fw-bold">@T["Personal information"]</h6>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Title"]</label>
|
||||
<input type="text" id="updateTitle" name="Title" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Name"]</label>
|
||||
<input type="text" id="updateName" name="Cn" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Surname"]</label>
|
||||
<input type="text" id="updateSurname" name="Sn" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Birth Date"]</label>
|
||||
<input type="text" id="updateBirthdate" name="Description.BirthDate" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["City"]</label>
|
||||
<input type="text" id="updateAddressCity" name="Description.Address.City" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Street"]</label>
|
||||
<input type="text" id="updateAddressStreet" name="Description.Address.Street" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Street Nr."]</label>
|
||||
<input type="text" id="updateAddressStreetNr" name="Description.Address.StreetNr" class="form-control" />
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<h6 class="fw-bold">@T["Workplace & account"]</h6>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Workplace"]</label>
|
||||
<input type="text" id="updateWorkplace" name="Description.Workplace" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Groups"]</label>
|
||||
<select id="updateGroups" name="Description.Groups" class="form-select" multiple></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["New Password"]</label>
|
||||
<input type="password" id="updatePassword" name="UserPassword" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Photo"]</label>
|
||||
<input type="file" id="updatePhotoFile" accept="image/*" class="form-control" />
|
||||
<input type="hidden" id="updateJpegPhoto" name="JpegPhoto" />
|
||||
<div class="mt-2 text-center">
|
||||
<img id="updatePhotoPreview" src="" alt="Preview" class="img-thumbnail" style="max-height: 150px; display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@T["Cancel"]</button>
|
||||
<button type="submit" form="updateForm" class="btn btn-warning">@T["Save changes"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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) {
|
||||
// Last part — assign value
|
||||
try {
|
||||
// Try to parse JSON strings like "[...]" if possible
|
||||
current[part] = JSON.parse(value);
|
||||
} catch {
|
||||
current[part] = value;
|
||||
}
|
||||
} else {
|
||||
// Intermediate part — create object if needed
|
||||
current[part] = current[part] || {};
|
||||
current = current[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- Update user modal ---
|
||||
const updateModal = document.getElementById('updateModal');
|
||||
const updateForm = document.getElementById('updateForm');
|
||||
let updateButton = null;
|
||||
|
||||
updateModal.addEventListener('show.bs.modal', event => {
|
||||
updateButton = event.relatedTarget;
|
||||
const userId = updateButton.getAttribute('data-user-id');
|
||||
const title = updateButton.getAttribute('data-user-title');
|
||||
const name = updateButton.getAttribute('data-user-name');
|
||||
const surname = updateButton.getAttribute('data-user-surname');
|
||||
const workplace = updateButton.getAttribute('data-user-workplace');
|
||||
const birthdate = updateButton.getAttribute('data-user-birthdate');
|
||||
const addressCity = updateButton.getAttribute('data-user-address-city');
|
||||
const addressStreet = updateButton.getAttribute('data-user-address-street');
|
||||
const addressStreetNr = updateButton.getAttribute('data-user-address-streetnr');
|
||||
|
||||
// Fill form fields
|
||||
updateForm.querySelector('#updateUid').value = userId;
|
||||
updateForm.querySelector('#updateTitle').value = title || '';
|
||||
updateForm.querySelector('#updateName').value = name || '';
|
||||
updateForm.querySelector('#updateSurname').value = surname || '';
|
||||
updateForm.querySelector('#updateWorkplace').value = workplace || '';
|
||||
updateForm.querySelector('#updateBirthdate').value = birthdate || '';
|
||||
updateForm.querySelector('#updateAddressCity').value = addressCity || '';
|
||||
updateForm.querySelector('#updateAddressStreet').value = addressStreet || '';
|
||||
updateForm.querySelector('#updateAddressStreetNr').value = addressStreetNr || '';
|
||||
updateForm.querySelector('#updatePassword').value = '';
|
||||
updateForm.querySelector('#updateJpegPhoto').value = '';
|
||||
});
|
||||
|
||||
updateForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
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);
|
||||
try {
|
||||
const response = await fetch('/Users/Update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.Success) {
|
||||
// Close modal
|
||||
bootstrap.Modal.getInstance(updateModal).hide();
|
||||
|
||||
// Update table row
|
||||
const row = updateButton.closest('tr');
|
||||
row.querySelector('td:nth-child(2)').textContent = result.NewUid || '';
|
||||
row.querySelector('td:nth-child(3)').textContent = data.Title || '';
|
||||
row.querySelector('td:nth-child(4)').textContent = data.Cn || '';
|
||||
row.querySelector('td:nth-child(5)').textContent = data.Sn || '';
|
||||
row.querySelector('td:nth-child(6)').textContent = data.Description?.Workplace || '';
|
||||
// Update button data attributes, so the next edit uses the new data
|
||||
updateButton.setAttribute('data-user-id', result.NewUid || '');
|
||||
updateButton.setAttribute('data-user-title', data.Title || '');
|
||||
updateButton.setAttribute('data-user-name', data.Cn || '');
|
||||
updateButton.setAttribute('data-user-surname', data.Sn || '');
|
||||
updateButton.setAttribute('data-user-workplace', data.Description?.Workplace || '');
|
||||
updateButton.setAttribute('data-user-groups', JSON.stringify(data.Description?.Groups || []));
|
||||
|
||||
if (data.JpegPhoto && data.JpegPhoto.length > 0) {
|
||||
const img = row.querySelector('td:first-child img');
|
||||
if (img) {
|
||||
img.src = `data:image/jpeg;base64,${data.JpegPhoto}`;
|
||||
img.classList.add('border', 'border-success'); // optional visual feedback
|
||||
setTimeout(() => img.classList.remove('border', 'border-success'), 1000);
|
||||
}
|
||||
}
|
||||
updateForm.reset();
|
||||
document.getElementById('updatePhotoPreview').style.display = "none";
|
||||
showToast('@T["User updated successfully"]', 'success');
|
||||
} else {
|
||||
showToast(`${result.Exception || '@T["Update failed"]'}`, 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('@T["Error updating user"]', 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Group select drop-down
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const updateModal = document.getElementById('updateModal');
|
||||
const updateForm = document.getElementById('updateForm');
|
||||
const updateGroupsSelect = document.getElementById('updateGroups');
|
||||
let updateButton = null;
|
||||
|
||||
updateModal.addEventListener('show.bs.modal', async event => {
|
||||
updateButton = event.relatedTarget;
|
||||
|
||||
const userId = updateButton.getAttribute('data-user-id');
|
||||
const title = updateButton.getAttribute('data-user-title');
|
||||
const name = updateButton.getAttribute('data-user-name');
|
||||
const surname = updateButton.getAttribute('data-user-surname');
|
||||
const workplace = updateButton.getAttribute('data-user-workplace');
|
||||
const groups = JSON.parse(updateButton.getAttribute('data-user-groups'));
|
||||
|
||||
// Fill basic fields
|
||||
updateForm.querySelector('#updateUid').value = userId;
|
||||
updateForm.querySelector('#updateTitle').value = title || '';
|
||||
updateForm.querySelector('#updateName').value = name || '';
|
||||
updateForm.querySelector('#updateSurname').value = surname || '';
|
||||
updateForm.querySelector('#updateWorkplace').value = workplace || '';
|
||||
updateForm.querySelector('#updatePassword').value = '';
|
||||
updateForm.querySelector('#updateJpegPhoto').value = '';
|
||||
|
||||
// Fetch available groups
|
||||
await populateGroupsDropdown(updateGroupsSelect, groups);
|
||||
|
||||
// Fetch user's existing groups (you need to provide them via data-attribute or fetch)
|
||||
const existingGroups = updateButton.getAttribute('data-user-groups');
|
||||
if (existingGroups) {
|
||||
const selectedGroups = existingGroups.split(',');
|
||||
Array.from(updateGroupsSelect.options).forEach(opt => {
|
||||
opt.selected = selectedGroups.includes(opt.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function populateGroupsDropdown(selectElement, preselectedGroups) {
|
||||
try {
|
||||
const res = await fetch('/Groups/Get');
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
console.warn('Failed to fetch groups', json);
|
||||
return;
|
||||
}
|
||||
selectElement.innerHTML = '';
|
||||
json.GroupModels.forEach(group => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = group.Cn;
|
||||
opt.textContent = group.DisplayName || group.Cn;
|
||||
selectElement.appendChild(opt);
|
||||
if (preselectedGroups != null && preselectedGroups.includes(group.Cn)) {
|
||||
setTimeout(() => {
|
||||
opt.selected = true;
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching groups', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const photoInput = document.getElementById('updatePhotoFile');
|
||||
const photoPreview = document.getElementById('updatePhotoPreview');
|
||||
const photoHidden = document.getElementById('updateJpegPhoto');
|
||||
|
||||
photoInput.addEventListener('change', () => {
|
||||
const file = photoInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const base64 = e.target.result.split(',')[1]; // strip data:image/jpeg;base64,
|
||||
photoHidden.value = base64;
|
||||
photoPreview.src = e.target.result;
|
||||
photoPreview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<!-- User Create Modal -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="createModalLabel">@T["Create User"]</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="createForm">
|
||||
<div class="row g-3">
|
||||
<h6 class="fw-bold">@T["Personal information"]</h6>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Title"]</label>
|
||||
<input type="text" name="Title" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Name"]</label>
|
||||
<input type="text" name="Cn" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Surname"]</label>
|
||||
<input type="text" name="Sn" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Birth Date"]</label>
|
||||
<input type="text" name="Description.BirthDate" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["City"]</label>
|
||||
<input type="text" name="Description.Address.City" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Street"]</label>
|
||||
<input type="text" name="Description.Address.Street" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Street Nr."]</label>
|
||||
<input type="text" name="Description.Address.StreetNr" class="form-control" />
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<h6 class="fw-bold">@T["Workplace & account"]</h6>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Workplace"]</label>
|
||||
<input type="text" name="Description.Workplace" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Groups"]</label>
|
||||
<select id="createGroups" name="Description.Groups" class="form-select" multiple></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Password"]</label>
|
||||
<input type="password" name="UserPassword" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Photo"]</label>
|
||||
<input type="file" id="createPhotoFile" accept="image/*" class="form-control" />
|
||||
<input type="hidden" id="createJpegPhoto" name="JpegPhoto" />
|
||||
<div class="mt-2 text-center">
|
||||
<img id="createPhotoPreview" src="" alt="Preview" class="img-thumbnail" style="max-height: 150px; display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@T["Cancel"]</button>
|
||||
<button type="submit" form="createForm" class="btn btn-primary">@T["Create"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const createModal = document.getElementById('createModal');
|
||||
const createForm = document.getElementById('createForm');
|
||||
const createGroupsSelect = document.getElementById('createGroups');
|
||||
const photoInput = document.getElementById('createPhotoFile');
|
||||
const photoPreview = document.getElementById('createPhotoPreview');
|
||||
const photoHidden = document.getElementById('createJpegPhoto');
|
||||
|
||||
// Load available groups when modal is shown
|
||||
createModal.addEventListener('show.bs.modal', async () => {
|
||||
await populateGroupsDropdown(createGroupsSelect);
|
||||
createForm.reset();
|
||||
photoPreview.style.display = 'none';
|
||||
});
|
||||
|
||||
// Handle photo upload preview
|
||||
photoInput.addEventListener('change', () => {
|
||||
const file = photoInput.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const base64 = e.target.result.split(',')[1];
|
||||
photoHidden.value = base64;
|
||||
photoPreview.src = e.target.result;
|
||||
photoPreview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Submit create form
|
||||
createForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const dataFromEntries = Object.fromEntries(new FormData(createForm).entries());
|
||||
const data = unflatten(dataFromEntries);
|
||||
data.Description.Groups = Array.from(createGroupsSelect.selectedOptions).map(o => o.value);
|
||||
|
||||
try {
|
||||
const response = await fetch('/Users/Create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.Success) {
|
||||
bootstrap.Modal.getInstance(createModal).hide();
|
||||
showToast('@T["User created successfully"]', 'success');
|
||||
|
||||
// Add new row to table dynamically
|
||||
const tbody = document.querySelector('table tbody');
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.innerHTML = `
|
||||
<td><img class="rounded-circle user-icon" src="data:image/jpeg;base64,${data.JpegPhoto || ''}" alt="Photo" style="max-width:300px;" /></td>
|
||||
<td>${result.NewUid || ''}</td>
|
||||
<td>${data.Title || ''}</td>
|
||||
<td>${data.Cn || ''}</td>
|
||||
<td>${data.Sn || ''}</td>
|
||||
<td>${data.Description?.Workplace || ''}</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-warning"
|
||||
data-user-id="${result.Uid || ''}"
|
||||
data-user-title="${data.Title || ''}"
|
||||
data-user-name="${data.Cn || ''}"
|
||||
data-user-surname="${data.Sn || ''}"
|
||||
data-user-workplace="${data.Description?.Workplace || ''}"
|
||||
data-user-groups='${JSON.stringify(data.Description?.Groups || [])}'
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#updateModal">
|
||||
Update
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete"
|
||||
data-user-id="${result.NewUid || ''}"
|
||||
data-user-name="${data.Cn || ''}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(newRow);
|
||||
} else {
|
||||
showToast(`${result.Exception || '@T["Create failed"]'}`, 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('@T["Error creating user"]', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
async function populateGroupsDropdown(selectElement) {
|
||||
try {
|
||||
const res = await fetch('/Groups/Get');
|
||||
const json = await res.json();
|
||||
if (!json.success) return;
|
||||
selectElement.innerHTML = '';
|
||||
json.GroupModels.forEach(group => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = group.Cn;
|
||||
opt.textContent = group.DisplayName || group.Cn;
|
||||
selectElement.appendChild(opt);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching groups', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<!-- User Detail Modal -->
|
||||
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-white">
|
||||
<h5 class="modal-title" id="detailModalLabel">@T["User Details"]</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4 text-center">
|
||||
<img id="detailPhoto" class="img-thumbnail rounded-circle" style="max-height:150px;" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Username"]</label>
|
||||
<input type="text" class="form-control" id="detailUid" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Title"]</label>
|
||||
<input type="text" class="form-control" id="detailTitle" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Name"]</label>
|
||||
<input type="text" class="form-control" id="detailName" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Surname"]</label>
|
||||
<input type="text" class="form-control" id="detailSurname" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Birth Date"]</label>
|
||||
<input type="text" class="form-control" id="detailBirthdate" value="" disabled />
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<h6 class="fw-bold">@T["Address"]</h6>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">@T["City"]</label>
|
||||
<input type="text" class="form-control" id="detailCity" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Street"]</label>
|
||||
<input type="text" class="form-control" id="detailStreet" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">@T["Street Nr."]</label>
|
||||
<input type="text" class="form-control" id="detailStreetNr" value="" disabled />
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<h6 class="fw-bold">@T["Workplace & account"]</h6>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Workplace"]</label>
|
||||
<input type="text" class="form-control" id="detailWorkplace" value="" disabled />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">@T["Groups"]</label>
|
||||
<input type="text" class="form-control" id="detailGroups" value="" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const rows = document.querySelectorAll('table tbody tr');
|
||||
const detailModalEl = document.getElementById('detailModal');
|
||||
const detailModal = new bootstrap.Modal(detailModalEl);
|
||||
|
||||
rows.forEach(row => {
|
||||
row.addEventListener('click', (e) => {
|
||||
// Don’t trigger when clicking inside the action buttons
|
||||
if (e.target.closest('button')) return;
|
||||
|
||||
const updateBtn = row.querySelector('.btn-warning[data-user-id]');
|
||||
if (!updateBtn) return;
|
||||
|
||||
// Extract data from update button
|
||||
const data = {
|
||||
uid: updateBtn.dataset.userId,
|
||||
title: updateBtn.dataset.userTitle,
|
||||
name: updateBtn.dataset.userName,
|
||||
surname: updateBtn.dataset.userSurname,
|
||||
workplace: updateBtn.dataset.userWorkplace,
|
||||
birthdate: updateBtn.dataset.userBirthdate,
|
||||
city: updateBtn.dataset.userAddressCity,
|
||||
street: updateBtn.dataset.userAddressStreet,
|
||||
streetNr: updateBtn.dataset.userAddressStreetnr,
|
||||
groups: JSON.parse(updateBtn.dataset.userGroups || '[]'),
|
||||
};
|
||||
|
||||
|
||||
// Fill modal fields
|
||||
document.getElementById('detailUid').value = data.uid || '';
|
||||
document.getElementById('detailTitle').value = data.title || '';
|
||||
document.getElementById('detailName').value = data.name || '';
|
||||
document.getElementById('detailSurname').value = data.surname || '';
|
||||
document.getElementById('detailBirthdate').value = data.birthdate || '';
|
||||
document.getElementById('detailCity').value = data.city || '';
|
||||
document.getElementById('detailStreet').value = data.street || '';
|
||||
document.getElementById('detailStreetNr').value = data.streetNr || '';
|
||||
document.getElementById('detailWorkplace').value = data.workplace || '';
|
||||
document.getElementById('detailGroups').value = data.groups.join(', ') || '';
|
||||
|
||||
// Photo
|
||||
const imgEl = row.querySelector('td:first-child img');
|
||||
const detailPhoto = document.getElementById('detailPhoto');
|
||||
if (imgEl && imgEl.src.startsWith('data:image')) {
|
||||
detailPhoto.src = imgEl.src;
|
||||
detailPhoto.style.display = 'block';
|
||||
} else {
|
||||
detailPhoto.style.display = 'none';
|
||||
}
|
||||
|
||||
detailModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.user-row > td:not(:last-child) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.user-row > td {
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
.user-row:has(td:not(:last-child):hover) > td {
|
||||
background-color: #17a2b8;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user