feat: Add Admin Groups Management Module
- Introduced a new module for managing user groups (`usrgrp`) in the administration interface. - Implemented CRUD operations for user groups: list, create, rename, and delete. - Added authorization checks to ensure only users with appropriate permissions can manage groups. - Created corresponding frontend JavaScript and SCSS files for the new module. - Updated documentation to reflect the new group management functionality and its authorization model. - Added SQL stored procedures for group management operations. - Established a new login script for database access, ensuring proper security practices.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OCORE.SQL;
|
||||
using static OCORE.commons;
|
||||
using static OCORE.OCORE_dictionaries;
|
||||
using static OCORE.SQL.sql;
|
||||
using static OCORE.web.mvc_helper_async;
|
||||
|
||||
namespace Fuchs.Controllers;
|
||||
|
||||
// Partial class: Administration user-group management module (fn=usrgrp).
|
||||
//
|
||||
// Split out as its own module (rather than folded into the usr module) per
|
||||
// Docs/Decisions/0021-user-group-management.md, following the pattern
|
||||
// Docs/Decisions/0015-split-admin-user-management-module.md already established for adding a new
|
||||
// admin-only capability. Shares the Admin/usr modules' fds_sys > 4 gate (AdminMinAuthExclusive,
|
||||
// GetSystemAdminAuthAsync, AdminJson — all in IntranetController.Admin.cs).
|
||||
//
|
||||
// The fis_admin_* stored procs/functions called below enforce the SAME SEPARATE, independent
|
||||
// authorization axis as the usr module: the per-user "intranet_admin" setting via
|
||||
// fis_admin_getUserAuth(@authaccount) > 1. An account with fds_sys > 4 but no intranet_admin
|
||||
// setting will reach these endpoints yet get empty results / no-op writes from SQL — accounts
|
||||
// doing group management need BOTH.
|
||||
public partial class IntranetController
|
||||
{
|
||||
private async Task<IActionResult> Do_Process_AdminGroups(string fn, string id, string code)
|
||||
{
|
||||
_logger.LogDebug("Do_Process_AdminGroups action={Action} code={Code} user={User}", id, code, UserAccountID);
|
||||
|
||||
int authLevel = await GetSystemAdminAuthAsync(fn, id, code);
|
||||
bool authorized = authLevel > AdminMinAuthExclusive;
|
||||
|
||||
if (id.Equals("auth", StringComparison.OrdinalIgnoreCase))
|
||||
return await JSONAsync(new { manage = authorized ? 1 : 0, level = authLevel });
|
||||
|
||||
if (!authorized)
|
||||
{
|
||||
_logger.LogWarning("AdminGroups access denied for user={User} (fds_sys={Level}) action={Action}",
|
||||
UserAccountID, authLevel, id);
|
||||
return Unauthorized401();
|
||||
}
|
||||
|
||||
switch (id.ToLowerInvariant())
|
||||
{
|
||||
case "list":
|
||||
{
|
||||
var res = await getSQLDatatable_async(
|
||||
"SELECT * FROM [dbo].[fis_admin_getUserGroupList](@authaccount, NULL) ORDER BY [order];",
|
||||
_intranet.Intranet__SQLConnectionString,
|
||||
StdParamlist(SQL_VarChar("@authaccount", UserAccountID)),
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
return AdminJson(new { groups = res.DataTable.toArrayofObjectDictionaries() });
|
||||
}
|
||||
|
||||
case "create":
|
||||
case "rename":
|
||||
{
|
||||
if (!HasForm("name")) return BadRequest400();
|
||||
string name = Form("name");
|
||||
string? groupId = id.Equals("rename", StringComparison.OrdinalIgnoreCase) ? Form("usergroup_id") : null;
|
||||
if (id.Equals("rename", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(groupId))
|
||||
return BadRequest400();
|
||||
|
||||
var pl = StdParamlist(
|
||||
SQL_VarChar("@authaccount", UserAccountID),
|
||||
SQL_VarChar("@usergroup_id", groupId, dbNull_IfEmpty: true),
|
||||
SQL_NVarChar("@name", name));
|
||||
|
||||
System.Data.DataRow? savedRow;
|
||||
try
|
||||
{
|
||||
var res = await getSQLDatatable_async(
|
||||
"EXECUTE [dbo].[fis_admin_setUserGroup] @authaccount, @usergroup_id, @name, @enc_key;",
|
||||
_intranet.Intranet__SQLConnectionString, pl,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
savedRow = res.Count > 0 ? res.FirstRow : null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAdminUserActionFailedAsync("Gruppe speichern fehlgeschlagen",
|
||||
"Eine Benutzergruppe konnte nicht gespeichert werden.", groupId ?? "", ex);
|
||||
return StatusCode(500, new { error = "save_failed" });
|
||||
}
|
||||
|
||||
if (savedRow == null)
|
||||
{
|
||||
_logger.LogWarning("AdminGroups {Action} not successful group={Group} user={User}", id, groupId, UserAccountID);
|
||||
return StatusCode(500, new { error = "save_failed" });
|
||||
}
|
||||
|
||||
_logger.LogInformation("AdminGroups {Action} succeeded group={Group} user={User}",
|
||||
id, savedRow.nz("usergroup_id"), UserAccountID);
|
||||
return AdminJson(new { ok = true, usergroup_id = savedRow.nz("usergroup_id"), name = savedRow.nz("name") });
|
||||
}
|
||||
|
||||
case "delete":
|
||||
{
|
||||
if (!HasForm("usergroup_id")) return BadRequest400();
|
||||
string groupId = Form("usergroup_id");
|
||||
|
||||
bool success;
|
||||
try
|
||||
{
|
||||
var res = await getSQLDatatable_async(
|
||||
"EXECUTE [dbo].[fis_admin_delUserGroup] @authaccount, @usergroup_id;",
|
||||
_intranet.Intranet__SQLConnectionString,
|
||||
StdParamlist(SQL_VarChar("@authaccount", UserAccountID), SQL_VarChar("@usergroup_id", groupId)),
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
success = res.Count > 0 && res.FirstRow[0] is true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAdminUserActionFailedAsync("Gruppe löschen fehlgeschlagen",
|
||||
$"Die Benutzergruppe {groupId} konnte nicht gelöscht werden.", groupId, ex);
|
||||
return StatusCode(500, new { error = "delete_failed" });
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
_logger.LogWarning("AdminGroups delete not successful group={Group} user={User}", groupId, UserAccountID);
|
||||
return StatusCode(500, new { error = "delete_failed" });
|
||||
}
|
||||
|
||||
_logger.LogInformation("AdminGroups delete succeeded group={Group} user={User}", groupId, UserAccountID);
|
||||
return AdminJson(new { ok = true });
|
||||
}
|
||||
|
||||
default:
|
||||
_logger.LogWarning("AdminGroups: no handler for action={Action}, user={User}", id, UserAccountID);
|
||||
return BadRequest400();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Web;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using OCORE.SQL;
|
||||
@@ -44,10 +44,16 @@ public partial class IntranetController
|
||||
{
|
||||
case "list":
|
||||
{
|
||||
// Optional useraccount_id narrows the result to a single account so the frontend can
|
||||
// refresh one row in place (e.g. after an email or group-membership change) instead
|
||||
// of re-rendering the whole grid and losing the current selection.
|
||||
string? filterId = HasForm("useraccount_id") ? Form("useraccount_id") : null;
|
||||
var res = await getSQLDatatable_async(
|
||||
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, NULL, NULL, NULL) ORDER BY [order];",
|
||||
"SELECT * FROM [dbo].[fis_admin_getUserAccountList](@authaccount, @useraccount_id, NULL, NULL) ORDER BY [order];",
|
||||
_intranet.Intranet__SQLConnectionString,
|
||||
StdParamlist(SQL_VarChar("@authaccount", UserAccountID)),
|
||||
StdParamlist(
|
||||
SQL_VarChar("@authaccount", UserAccountID),
|
||||
SQL_VarChar("@useraccount_id", filterId, dbNull_IfEmpty: true)),
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
return AdminJson(new { users = res.DataTable.toArrayofObjectDictionaries() });
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
"bam" => await Do_Process_Bankings(fn, id, code),
|
||||
"admin" => await Do_Process_Admin(fn, id, code),
|
||||
"usr" => await Do_Process_AdminUsers(fn, id, code),
|
||||
"usrgrp" => await Do_Process_AdminGroups(fn, id, code),
|
||||
"auth" => await HandleAuth(fn, id, code),
|
||||
"spwc" => await HandleSendPasswordCode(fn, id, code),
|
||||
"spw" => await HandleSendPassword(fn, id, code),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
status: Active
|
||||
lastUpdated: 2026-09-16
|
||||
applyTo:
|
||||
- "Fuchs/Controllers/IntranetController.AdminGroups.cs"
|
||||
- "Fuchs/js/intranet/modules/fis.usrgrp*.js"
|
||||
- "Fuchs/js/intranet/modules/fis.usrgrp.scss"
|
||||
- "Fuchs/js/intranet/fis_main_menu.js"
|
||||
- "Fuchs_Database/dbo/Functions/fis_admin_getUserGroupList.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_setUserGroup.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserGroup.sql"
|
||||
relatedDecisions:
|
||||
- "0021-user-group-management.md"
|
||||
- "0014-admin-user-management.md"
|
||||
- "0015-split-admin-user-management-module.md"
|
||||
---
|
||||
|
||||
# Group Management module (`usrgrp`)
|
||||
|
||||
## Summary
|
||||
The **Gruppenverwaltung** module lets a privileged operator see every non-deleted usergroup,
|
||||
create a new one, rename it, and delete it. It's the catalog-management counterpart to the `usr`
|
||||
module's per-user "Gruppen" dialog, which toggles an individual user's membership in these groups
|
||||
but cannot create/rename/delete the groups themselves. Same sidemenu-visibility and server-side
|
||||
authorization gate as `usr`/`admin` (`fds_sys > 4`), own `fn=usrgrp`, own frontend bundle, own
|
||||
controller partial. See [0021](../Decisions/0021-user-group-management.md) for why this is a
|
||||
separate module rather than folded into `usr`.
|
||||
|
||||
## How it works
|
||||
|
||||
### Authorization (two independent axes — same as `usr`, read [0014](../Decisions/0014-admin-user-management.md) first)
|
||||
1. **Menu visibility + `fds_sys` gate** — `fis_main_menu.js#addAdminMenuButtonsIfAuthorized`
|
||||
checks `fds_sys > 4` once and, if authorized, adds the "Administration" sidemenu dropdown with
|
||||
three entries: Benutzerverwaltung (`init:usr`), Gruppenverwaltung (`init:usrgrp`) and
|
||||
Systemüberwachung (`init:admin`). Every `/do/usrgrp/*` endpoint re-checks `fds_sys > 4`
|
||||
server-side (`Do_Process_AdminGroups`, reusing `AdminMinAuthExclusive`/`GetSystemAdminAuthAsync`
|
||||
from `IntranetController.Admin.cs`) and 401s otherwise.
|
||||
2. **Separate SQL-layer gate** — every `fis_admin_*` proc/function called below independently
|
||||
checks the caller's `intranet_admin` setting via `fis_admin_getUserAuth(@authaccount) > 1`. An
|
||||
account with `fds_sys > 4` but no `intranet_admin` setting reaches every `usrgrp` endpoint but
|
||||
gets empty results / silent no-op writes — not an error. This also applies to
|
||||
`fis_admin_addGroupMember`/`remGroupMember` (used by the `usr` module's membership dialog,
|
||||
documented there) — all five group-related `fis_admin_*` write/read objects now carry this
|
||||
check as of [0021](../Decisions/0021-user-group-management.md), which closed a real gap where
|
||||
three of them had none.
|
||||
|
||||
### Request flow
|
||||
```
|
||||
click "Gruppenverwaltung" → $ocms.init('usrgrp')
|
||||
POST /do/usrgrp/auth → { manage, level } (manage>0 ⇒ authorized)
|
||||
load /web/fis.usrgrp.de.js + /web/fis.usrgrp.css
|
||||
$ocms.usrgrp.init2() → init3() → renderLst()
|
||||
POST /do/usrgrp/list → { groups: [...] } every non-deleted group
|
||||
base ribbon (eM) → "Anlegen" (always available)
|
||||
POST /do/usrgrp/create {name} → { ok, usergroup_id, name }
|
||||
row click → per-row ribbon (iMn): Umbenennen / Löschen
|
||||
POST /do/usrgrp/rename {usergroup_id, name} → { ok, usergroup_id, name }
|
||||
POST /do/usrgrp/delete {usergroup_id} → { ok }
|
||||
```
|
||||
|
||||
### Backend (`IntranetController.AdminGroups.cs`)
|
||||
- `list` → `fis_admin_getUserGroupList(@authaccount, NULL)`, all non-deleted groups with
|
||||
decrypted names.
|
||||
- `create`/`rename` → both call `fis_admin_setUserGroup(@authaccount, @usergroup_id, @name,
|
||||
@enc_key)`; `create` passes `@usergroup_id = NULL` (the proc generates a new id via
|
||||
`fis_fn_id`), `rename` passes the existing id.
|
||||
- `delete` → `fis_admin_delUserGroup(@authaccount, @usergroup_id)` (soft-delete: sets
|
||||
`UserDeleted`/`DateDeleted`). No check for remaining members — a deleted group's membership
|
||||
rows simply stop being returned by `fis_admin_getUserGroupMembership` (filtered on the group's
|
||||
own `DateDeleted`), see [0021](../Decisions/0021-user-group-management.md) Consequences.
|
||||
|
||||
### Frontend (`fis.usrgrp.js` + `fis.usrgrp_txt_de.js` + `fis.usrgrp.scss`)
|
||||
Same list+ribbon pattern as `fis.usr.js`: `renderLst` builds a single-column card-style row list;
|
||||
a row click toggles `.selected` and swaps the `#topbar` ribbon between the base ribbon (`eM`:
|
||||
"Anlegen") and the per-row actions (`iMn`: Umbenennen/Löschen). `createDlg`/`renameDlg` use
|
||||
`$ocms.dlgform`; `deleteGroup` follows the `confirm(...)`-before-destructive-action pattern (no
|
||||
dialog, nothing to collect beyond the confirmation).
|
||||
|
||||
## Key files
|
||||
- `Fuchs/Controllers/IntranetController.AdminGroups.cs` — `Do_Process_AdminGroups` dispatch.
|
||||
- `Fuchs/Controllers/IntranetController.Admin.cs` — shared `AdminMinAuthExclusive`/
|
||||
`GetSystemAdminAuthAsync`/`AdminJson` this module reuses.
|
||||
- `Fuchs/js/intranet/modules/fis.usrgrp.js`, `fis.usrgrp_txt_de.js`, `fis.usrgrp.scss` — the
|
||||
module, texts, styles (bundled via `bdlconfig.json`, `intranet:usrgrp` context).
|
||||
- `Fuchs/js/intranet/fis_main_menu.js` (`addAdminMenuButtonsIfAuthorized`) — sidemenu wiring.
|
||||
- `Fuchs_Database/dbo/Functions/fis_admin_getUserGroupList.sql` — read-side group query.
|
||||
- `Fuchs_Database/dbo/Stored Procedures/fis_admin_setUserGroup.sql`,
|
||||
`fis_admin_delUserGroup.sql` — write-side group procs.
|
||||
|
||||
## Related decisions
|
||||
- [0021 — User group management (new module, closed the missing intranet_admin gate)](../Decisions/0021-user-group-management.md)
|
||||
- [0014 — Admin user management (dual auth axis, original decisions)](../Decisions/0014-admin-user-management.md)
|
||||
- [0015 — Split Admin into separate Status/User-Management modules](../Decisions/0015-split-admin-user-management-module.md)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: Active
|
||||
lastUpdated: 2026-09-14
|
||||
lastUpdated: 2026-09-16
|
||||
applyTo:
|
||||
- "Fuchs/Controllers/IntranetController.AdminUsers.cs"
|
||||
- "Fuchs/js/intranet/modules/fis.usr*.js"
|
||||
@@ -12,6 +12,7 @@ relatedDecisions:
|
||||
- "0014-admin-user-management.md"
|
||||
- "0015-split-admin-user-management-module.md"
|
||||
- "0020-user-management-create-and-delete.md"
|
||||
- "0021-user-group-management.md"
|
||||
---
|
||||
|
||||
# User Management module (`usr`)
|
||||
@@ -51,7 +52,8 @@ click "Benutzerverwaltung" → $ocms.init('usr')
|
||||
POST /do/usr/auth → { manage, level } (manage>0 ⇒ authorized)
|
||||
load /web/fis.usr.de.js + /web/fis.usr.css
|
||||
$ocms.usr.init2() → init3() → renderLst()
|
||||
POST /do/usr/list → { users: [...] } grid of every non-deleted account
|
||||
POST /do/usr/list → { users: [...] } grid of every non-deleted account (incl. `groups` csv)
|
||||
POST /do/usr/list {useraccount_id} → { users: [one] } single-row refresh (refreshRow)
|
||||
base ribbon (eM) → "Neuer Benutzer" (always available, not just when a row is selected)
|
||||
POST /do/usr/create {name, firstname, email, title?, gender?, mobile?} → { ok, useraccount_id, emailed } | 409 duplicate_email
|
||||
row click → per-row ribbon (iMn): Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit / Benutzer löschen
|
||||
@@ -63,8 +65,12 @@ click "Benutzerverwaltung" → $ocms.init('usr')
|
||||
```
|
||||
|
||||
### Backend (`IntranetController.AdminUsers.cs`)
|
||||
- `list` → `fis_admin_getUserAccountList(@authaccount, NULL, NULL, NULL)`, all non-deleted
|
||||
accounts with decrypted name/email/disabled flag.
|
||||
- `list` → `fis_admin_getUserAccountList(@authaccount, @useraccount_id, NULL, NULL)`, all non-deleted
|
||||
accounts with decrypted name/email/disabled flag plus a `groups` column — a `', '`-separated,
|
||||
alphabetically ordered csv of the account's active group memberships (`STRING_AGG` over
|
||||
decrypted `fis_usergroups.name_enc`, deleted groups/memberships excluded). An optional
|
||||
`useraccount_id` form field narrows the result to one account so the frontend can refresh a
|
||||
single row without re-rendering the grid.
|
||||
- `pwdreset` → the admin may type a specific password or leave it blank; validated with a plain
|
||||
`ValidatePassword(minLength: 6, numUpper: 0, numLower: 0, numNumbers: 0, numSpecial: 0)` — no
|
||||
hidden composition requirements, matching what the UI actually tells the admin (a prior bug
|
||||
@@ -82,7 +88,9 @@ click "Benutzerverwaltung" → $ocms.init('usr')
|
||||
changed (`@password = NULL`, so the `WHEN MATCHED` update path leaves `password_enc` alone). A
|
||||
duplicate email (`THROW 60001` in the proc) surfaces as HTTP 409 `{ error: "duplicate_email" }`.
|
||||
- `groups` → the new `fis_admin_getUserGroupMembership(@authaccount, @useraccount_id)` function
|
||||
returns every group plus a `member` bit for that one user.
|
||||
returns every group plus a `member` bit for that one user. The group catalog itself (create/
|
||||
rename/delete) is managed in the separate `usrgrp` module — see
|
||||
[admin-group-management.md](admin-group-management.md).
|
||||
- `groupset` → calls `fis_admin_addGroupMember`/`remGroupMember` depending on the posted
|
||||
`member` flag — applied immediately per checkbox toggle, no separate save step.
|
||||
- `create` → `fis_admin_createUserAccount(@authaccount, @name, @firstname, @title, @gender,
|
||||
@@ -101,7 +109,11 @@ click "Benutzerverwaltung" → $ocms.init('usr')
|
||||
### Frontend (`fis.usr.js` + `fis.usr_txt_de.js` + `fis.usr.scss`)
|
||||
A self-contained list+ribbon module modeled on Banking (`fis.bam.js`)'s `eM`/`iMn` pattern
|
||||
rather than the Admin dashboard's card layout: `renderLst` builds a card-style row grid (avatar
|
||||
initials badge, name/email columns, a status pill) rather than a plain `<table>`; a row click
|
||||
initials badge, name/email/groups columns, a status pill) rather than a plain `<table>`, with
|
||||
each row produced by `buildRow(rw)`. Changes that only affect one account (`emailDlg`, every
|
||||
checkbox toggle in `groupsDlg`) call `refreshRow(rw)`, which re-posts `usr/list` with that
|
||||
`useraccount_id` and swaps the row in place — selection and the per-row ribbon survive, and the
|
||||
groups csv is always current. Create/delete/password-reset still do a full `renderLst`. A row click
|
||||
toggles `.selected` and swaps the `#topbar` ribbon between the base ribbon (`eM`: "Neuer
|
||||
Benutzer" always available, plus "Neu Laden" once a row was selected) and the four per-row
|
||||
actions (`iMn`: Passwort zurücksetzen / E-Mail ändern / Gruppenzugehörigkeit / Benutzer löschen).
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
status: Accepted
|
||||
date: 2026-09-16
|
||||
applyTo:
|
||||
- "Fuchs/Controllers/IntranetController.AdminGroups.cs"
|
||||
- "Fuchs/js/intranet/modules/fis.usrgrp*.js"
|
||||
- "Fuchs/js/intranet/modules/fis.usrgrp.scss"
|
||||
- "Fuchs/js/intranet/fis_main_menu.js"
|
||||
- "Fuchs_Database/dbo/Functions/fis_admin_getUserGroupList.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_setUserGroup.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_delUserGroup.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_addGroupMember.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fis_admin_remGroupMember.sql"
|
||||
supersededBy: ""
|
||||
---
|
||||
|
||||
# 0021 — User group management: new `usrgrp` module, close the missing `intranet_admin` gate on group writes
|
||||
|
||||
## Context
|
||||
The per-user "Gruppen" dialog in the `usr` module ([0014](0014-admin-user-management.md),
|
||||
[0015](0015-split-admin-user-management-module.md)) lets an admin toggle an existing user's
|
||||
membership in each usergroup, but there was no UI anywhere to create, rename or delete a
|
||||
usergroup itself — `fis_usergroups` could only be populated by running a raw SQL script by hand
|
||||
(`hlp/db/create user group.sql`). The DB-layer proc `fis_admin_setUserGroup` (create/rename) and
|
||||
the read-side `fis_admin_getUserGroupList` already existed but were wired to no controller
|
||||
endpoint, mirroring the exact gap [0020](0020-user-management-create-and-delete.md) closed for
|
||||
user accounts.
|
||||
|
||||
While wiring this up, three of the four `fis_admin_*` group-write procedures were found missing
|
||||
the module's standard `intranet_admin` authorization check entirely (`fis_admin_addGroupMember`,
|
||||
`fis_admin_remGroupMember`, `fis_admin_delUserGroup`) — a caller with `fds_sys > 4` but no
|
||||
`intranet_admin` setting could add/remove a user's group membership or delete a group outright,
|
||||
the same real authorization hole 0020 fixed for `fis_admin_delUserAccount`. `fis_admin_setUserGroup`
|
||||
already had the check.
|
||||
|
||||
## Decision
|
||||
- **New standalone module, `fn=usrgrp`** (`IntranetController.AdminGroups.cs`,
|
||||
`fis.usrgrp.js`/`fis.usrgrp_txt_de.js`/`fis.usrgrp.scss`), added as a third entry in the existing
|
||||
"Administration" sidemenu dropdown alongside Benutzerverwaltung and Systemüberwachung — per
|
||||
[0015](0015-split-admin-user-management-module.md)'s own stated consequence that any future
|
||||
admin-only feature should default to its own module under that dropdown rather than be folded
|
||||
into an existing one. The dropdown itself already stays visible while any of its children is
|
||||
open, so switching between Gruppenverwaltung and Benutzerverwaltung needs no dedicated "back"
|
||||
control.
|
||||
- **Same dual-authorization gate as every other module under this dropdown**: `fds_sys > 4`
|
||||
server-side (`AdminMinAuthExclusive`/`GetSystemAdminAuthAsync`, shared from
|
||||
`IntranetController.Admin.cs`) gates whether the module is reachable/visible at all;
|
||||
`fis_admin_getUserAuth(@authaccount) > 1` (the `intranet_admin` setting) independently gates
|
||||
every read/write the `fis_admin_*` procs perform. Both together are what "only admins can see
|
||||
and change groups" means in this codebase — see the Consequences below for why this is enforced
|
||||
in four places, not one.
|
||||
- **New `id` cases in `Do_Process_AdminGroups`**: `list` → `fis_admin_getUserGroupList`, `create`/
|
||||
`rename` → `fis_admin_setUserGroup` (same proc; `rename` supplies an existing `@usergroup_id`),
|
||||
`delete` → `fis_admin_delUserGroup`. No membership editing here — that stays in the `usr`
|
||||
module's existing per-user checkbox dialog, unchanged.
|
||||
- **Closed the missing `intranet_admin` gate** on `fis_admin_addGroupMember`,
|
||||
`fis_admin_remGroupMember` and `fis_admin_delUserGroup` by adding the same
|
||||
`fis_admin_getUserAuth(@authaccount) > 1` guard `fis_admin_setUserGroup` already had, following
|
||||
the same "no-op instead of error" style 0020 used for `fis_admin_delUserAccount`: a caller
|
||||
without `intranet_admin >= 2` gets an unmodified result set / a `false` success bit, not a SQL
|
||||
error, consistent with every other `fis_admin_*` proc in this area.
|
||||
- **Frontend** mirrors `fis.usr.js`'s list+ribbon pattern (`renderLst`/`eM`/`iMn`): a card-style
|
||||
row list with a base ribbon button ("Anlegen") and a per-row ribbon ("Umbenennen"/"Löschen") on
|
||||
row selection, using `$ocms.dlgform` for create/rename and a `confirm(...)` guard (no dialog) for
|
||||
delete — the same choice 0020 made for deleting a user account.
|
||||
|
||||
## Consequences
|
||||
- Deleting a group does **not** guard against it still having members — its `fis_usergroups_members`
|
||||
rows simply stop being returned (the membership query already filters on the group's own
|
||||
`DateDeleted`). No UI currently surfaces "this group has N members" before deletion; acceptable
|
||||
since deletion is a soft-delete and the group row can be recreated with the same effective
|
||||
membership restored via `fis_admin_addGroupMember` if this turns out to matter in practice.
|
||||
- Four separate `fis_admin_*` procs/functions now each independently re-check
|
||||
`intranet_admin > 1` (`getUserGroupList`, `setUserGroup`, `delUserGroup`, `addGroupMember`,
|
||||
`remGroupMember` — five, in fact) rather than a single shared gate; this is the established
|
||||
style in this codebase (every `fis_admin_*` object re-checks for itself, see 0014) and not
|
||||
something this change revisits.
|
||||
- Same operational note as 0014/0015/0020: an account needs **both** `fds_sys > 4` and
|
||||
`intranet_admin >= 2` to actually manage groups, not obvious from the UI.
|
||||
|
||||
## Alternatives considered
|
||||
- **Extend the existing per-user "Gruppen" dialog** with an inline "new group" input and a
|
||||
delete icon per row, instead of a new module. Rejected per explicit user direction — the user
|
||||
asked for a dedicated sidemenu entry for group management, separate from the per-user dialog.
|
||||
- **Fold into the `usr` module** as a second ribbon button. Rejected for the same reason 0015
|
||||
split `usr` off `admin` in the first place: two unrelated concerns (managing individual user
|
||||
accounts vs. managing the group catalog) behind one button was already identified as a mistake
|
||||
once it happened once; the "own module under the dropdown" default from 0015 applies directly.
|
||||
@@ -191,5 +191,24 @@
|
||||
"js/intranet/modules/fis.usr.scss"
|
||||
],
|
||||
"minify": { "enabled": true }
|
||||
},
|
||||
{
|
||||
"context": "intranet:usrgrp",
|
||||
"outputFileName": "wwwroot/web/fis.usrgrp.de.js",
|
||||
"inputFiles": [
|
||||
"js/intranet/modules/fis.usrgrp_txt_de.js",
|
||||
"js/intranet/modules/fis.usrgrp.js"
|
||||
],
|
||||
"minify": { "enabled": true }
|
||||
},
|
||||
{
|
||||
"context": "intranet:usrgrp",
|
||||
"outputFileName": "wwwroot/web/fis.usrgrp.min.css",
|
||||
"inputFiles": [
|
||||
"css/intranet/oci_variables.scss",
|
||||
"css/intranet/fis_variables.scss",
|
||||
"js/intranet/modules/fis.usrgrp.scss"
|
||||
],
|
||||
"minify": { "enabled": true }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
})();
|
||||
|
||||
/* "Administration" is a single sidemenu dropdown (ocmsmenu supports nested items via `itm`)
|
||||
containing two buttons — Benutzerverwaltung (fn=usr) and Systemüberwachung (fn=admin) — both
|
||||
restricted to users whose fds_sys authorization exceeds 4. One auth check gates the whole
|
||||
dropdown; it (and therefore both module scripts) is only added after that check resolves, so
|
||||
lower-privileged users never see it or fetch either module's code.
|
||||
containing three buttons — Benutzerverwaltung (fn=usr), Gruppenverwaltung (fn=usrgrp) and
|
||||
Systemüberwachung (fn=admin) — all restricted to users whose fds_sys authorization exceeds 4.
|
||||
One auth check gates the whole dropdown; it (and therefore all module scripts) is only added
|
||||
after that check resolves, so lower-privileged users never see it or fetch any module's code.
|
||||
Called from fis_main_go.js once the DOM (and the initial main menu) is ready. */
|
||||
$fis.addAdminMenuButtonsIfAuthorized = function () {
|
||||
if ($('#m_admin_grp').length > 0) { return; } // idempotent
|
||||
@@ -25,6 +25,7 @@ $fis.addAdminMenuButtonsIfAuthorized = function () {
|
||||
lbl: $t.m_admin, id: 'm_admin_grp', ico: 'glyphicon glyphicon-cog',
|
||||
itm: [
|
||||
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' },
|
||||
{ lbl: $t.m_usrgrp, id: 'm_usrgrp', fnc: 'init:usrgrp', ico: 'glyphicon glyphicon-list' },
|
||||
{ lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard' }
|
||||
]
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
, m_admin: 'Administration'
|
||||
, m_admin_status: 'Systemüberwachung'
|
||||
, m_usr: 'Benutzerverwaltung'
|
||||
, m_usrgrp: 'Gruppenverwaltung'
|
||||
, rsp: 'Passwort ändern'
|
||||
, pnm: 'Die Passwörter stimmen nicht überein'
|
||||
, cps: 'Das neue Passwort wurde gespeichert.'
|
||||
|
||||
@@ -30,6 +30,40 @@ let $usr = {
|
||||
let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0);
|
||||
return (f + n).toUpperCase() || '?';
|
||||
},
|
||||
/* Builds one (detached) grid row for a user record; shared by renderLst and refreshRow. */
|
||||
buildRow: function (rw) {
|
||||
let disabled = bool(rw.disabled, false) === true;
|
||||
let row = $$.dc('usr_row', { id: 'usr' + rw.useraccount_id });
|
||||
$$.dc('usr_avatar', row).text($usr.initials(rw.firstname, rw.name));
|
||||
$$.dc('usr_col_name', row).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
|
||||
$$.dc('usr_col_email', row).text(rw.email || '');
|
||||
$$.dc('usr_col_groups', row).text(rw.groups || '').attr('title', rw.groups || '');
|
||||
$$.dc('usr_col_status', row).append(
|
||||
$$.dc(disabled ? 'usr_pill usr_pill_off' : 'usr_pill usr_pill_on').text(disabled ? $uct.disabled : $uct.active)
|
||||
);
|
||||
row.click(function () {
|
||||
row.toggleClass('selected').siblings().rC('selected');
|
||||
if (row.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); }
|
||||
});
|
||||
return row;
|
||||
},
|
||||
/* Re-fetches a single user and swaps its grid row in place, keeping selection + ribbon
|
||||
(used after changes that only affect that one row: email, group membership). Falls back
|
||||
to a full reload when the row is no longer in the grid. */
|
||||
refreshRow: function (rw) {
|
||||
let old = $('#usr' + rw.useraccount_id);
|
||||
if (old.length === 0) { $usr.renderLst(); return; }
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usr/list'), data: { useraccount_id: rw.useraccount_id }, success: function (r) {
|
||||
let fresh = (r.users || [])[0];
|
||||
if (!fresh) { $usr.renderLst(); return; }
|
||||
let selected = old.is('.selected');
|
||||
let row = $usr.buildRow(fresh).toggleClass('selected', selected);
|
||||
old.replaceWith(row);
|
||||
if (selected) { $usr.iMn(fresh); }
|
||||
}, error: function () { $usr.renderLst(); }
|
||||
});
|
||||
},
|
||||
renderLst: function () {
|
||||
let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst);
|
||||
$usr.eM(true);
|
||||
@@ -41,25 +75,13 @@ let $usr = {
|
||||
$$.dc('usr_col_avatar', hdr);
|
||||
$$.dc('usr_col_name', hdr).text($uct.col_name);
|
||||
$$.dc('usr_col_email', hdr).text($uct.col_email);
|
||||
$$.dc('usr_col_groups', hdr).text($uct.col_groups);
|
||||
$$.dc('usr_col_status', hdr).text($uct.col_status);
|
||||
if ((r.users || []).length === 0) {
|
||||
$$.dc('usr_empty', grid).text($uct.empty);
|
||||
return;
|
||||
}
|
||||
$.each(r.users || [], function (ri, rw) {
|
||||
let disabled = bool(rw.disabled, false) === true;
|
||||
let row = $$.dc('usr_row', grid, { id: 'usr' + rw.useraccount_id });
|
||||
$$.dc('usr_avatar', row).text($usr.initials(rw.firstname, rw.name));
|
||||
$$.dc('usr_col_name', row).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
|
||||
$$.dc('usr_col_email', row).text(rw.email || '');
|
||||
$$.dc('usr_col_status', row).append(
|
||||
$$.dc(disabled ? 'usr_pill usr_pill_off' : 'usr_pill usr_pill_on').text(disabled ? $uct.disabled : $uct.active)
|
||||
);
|
||||
row.click(function () {
|
||||
row.toggleClass('selected').siblings().rC('selected');
|
||||
if (row.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); }
|
||||
});
|
||||
});
|
||||
$.each(r.users || [], function (ri, rw) { grid.append($usr.buildRow(rw)); });
|
||||
}, error: function () {
|
||||
usrfrm.empty();
|
||||
$$.dc('usr_err', usrfrm).text($uct.loadfail);
|
||||
@@ -142,7 +164,7 @@ let $usr = {
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($uct.err[code] || $uct.err_generic);
|
||||
}, complete: function () { c.ldng(0); $usr.renderLst(); }
|
||||
}, complete: function () { c.ldng(0); $usr.refreshRow(rw); }
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -161,6 +183,7 @@ let $usr = {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usr/groupset'),
|
||||
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked },
|
||||
success: function () { $usr.refreshRow(rw); }, /* keep the groups csv in the grid current per toggle */
|
||||
error: function () {
|
||||
cb.prop('checked', !checked);
|
||||
alert($uct.err_generic);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
.usr_row {
|
||||
display: grid;
|
||||
grid-template-columns: 2.75rem 1.6fr 1.8fr 8rem;
|
||||
grid-template-columns: 2.75rem 1.4fr 1.6fr 1.6fr 8rem;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.55rem 1rem;
|
||||
@@ -57,6 +57,8 @@
|
||||
|
||||
.usr_col_name { font-weight: 600; color: #222; }
|
||||
.usr_col_email { color: #555; overflow-wrap: anywhere; }
|
||||
.usr_col_groups { color: #555; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.usr_row_hd .usr_col_groups { font-size: inherit; }
|
||||
|
||||
.usr_pill {
|
||||
display: inline-block;
|
||||
@@ -86,11 +88,12 @@
|
||||
.usrfrm {
|
||||
padding: 0.75rem;
|
||||
|
||||
.usr_row { grid-template-columns: 2.25rem 1fr; grid-template-areas: "avatar name" "avatar email" "avatar status"; }
|
||||
.usr_row { grid-template-columns: 2.25rem 1fr; grid-template-areas: "avatar name" "avatar email" "avatar groups" "avatar status"; }
|
||||
.usr_row_hd { display: none; }
|
||||
.usr_avatar { grid-area: avatar; }
|
||||
.usr_col_name { grid-area: name; }
|
||||
.usr_col_email { grid-area: email; font-size: 0.85rem; }
|
||||
.usr_col_groups { grid-area: groups; font-size: 0.85rem; white-space: normal; }
|
||||
.usr_col_status { grid-area: status; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ let $uct = {
|
||||
, rel: 'Neu Laden'
|
||||
, col_name: 'Name'
|
||||
, col_email: 'E-Mail'
|
||||
, col_groups: 'Gruppen'
|
||||
, col_status: 'Status'
|
||||
, active: 'Aktiv'
|
||||
, disabled: 'Deaktiviert'
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
let $ugp = {
|
||||
init2: function () {
|
||||
$ocms.getScript([], function () { $ugp.init3(); });
|
||||
},
|
||||
init3: function () {
|
||||
$fis.cf(true);
|
||||
$fis.lf(true);
|
||||
$('#activemodule').text($ugt.mdl);
|
||||
$ugp.eM();
|
||||
$ugp.renderLst();
|
||||
},
|
||||
/* Default ribbon (nothing selected). */
|
||||
eM: function (r) {
|
||||
let m = [{ lbl: $ugt.newgroup_btn, fnc: $ugp.createDlg }];
|
||||
if (bool(r, false) === true) { m.push({ lbl: $ugt.rel, fnc: $ugp.renderLst }); }
|
||||
return $('#topbar').ocmsmenu(m);
|
||||
},
|
||||
/* Per-row ribbon shown while a group row is selected. */
|
||||
iMn: function (rw) {
|
||||
return $('#topbar').ocmsmenu([
|
||||
{ lbl: $ugt.rename, fnc: function () { $ugp.renameDlg(rw); } },
|
||||
{ lbl: $ugt.delete, fnc: function () { $ugp.deleteGroup(rw); } }
|
||||
]);
|
||||
},
|
||||
renderLst: function () {
|
||||
let ugplst = $fis.frm_list(true, true).ldng(1), ugpfrm = $$.dc('ugpfrm', ugplst);
|
||||
$ugp.eM(true);
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/list'), success: function (r) {
|
||||
ugpfrm.empty();
|
||||
let grid = $$.dc('ugp_grid', ugpfrm);
|
||||
let hdr = $$.dc('ugp_row ugp_row_hd', grid);
|
||||
$$.dc('ugp_col_name', hdr).text($ugt.col_name);
|
||||
if ((r.groups || []).length === 0) {
|
||||
$$.dc('ugp_empty', grid).text($ugt.empty);
|
||||
return;
|
||||
}
|
||||
$.each(r.groups || [], function (gi, rw) {
|
||||
let row = $$.dc('ugp_row', grid, { id: 'ugp' + rw.usergroup_id });
|
||||
$$.dc('ugp_col_name', row).text(rw.name || '');
|
||||
row.click(function () {
|
||||
row.toggleClass('selected').siblings().rC('selected');
|
||||
if (row.is('.selected') === true) { $ugp.iMn(rw); } else { $ugp.eM(true); }
|
||||
});
|
||||
});
|
||||
}, error: function () {
|
||||
ugpfrm.empty();
|
||||
$$.dc('ugp_err', ugpfrm).text($ugt.loadfail);
|
||||
}, complete: function () { ugplst.ldng(0); }
|
||||
});
|
||||
},
|
||||
createDlg: function () {
|
||||
$ocms.dlgform([
|
||||
{ name: 'name', label: $ugt.name_field, type: 'string', required: true }
|
||||
], {
|
||||
title: $ugt.newgroup,
|
||||
button: $ugt.newgroup_btn,
|
||||
submit: function (e) {
|
||||
let c = $(this).ldng(1);
|
||||
let qs = c.serializeObject(true, { typedvalues: true });
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/create'), data: qs, success: function () {
|
||||
c.trigger('modal_close');
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($ugt.err[code] || $ugt.err_generic);
|
||||
}, complete: function () { c.ldng(0); $ugp.renderLst(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
renameDlg: function (rw) {
|
||||
$ocms.dlgform([
|
||||
{ name: 'usergroup_id', type: 'hidden', value: rw.usergroup_id },
|
||||
{ name: 'name', label: $ugt.name_field, type: 'string', required: true, value: rw.name }
|
||||
], {
|
||||
title: $ugt.rename,
|
||||
button: $ugt.rename_btn,
|
||||
submit: function (e) {
|
||||
let c = $(this).ldng(1);
|
||||
let qs = c.serializeObject(true, { typedvalues: true });
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/rename'), data: qs, success: function () {
|
||||
c.trigger('modal_close');
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($ugt.err[code] || $ugt.err_generic);
|
||||
}, complete: function () { c.ldng(0); $ugp.renderLst(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteGroup: function (rw) {
|
||||
if (confirm(string($ugt.delete_confirm, [rw.name || '']))) {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/delete'), data: { usergroup_id: rw.usergroup_id }, success: function () {
|
||||
$ugp.eM(true);
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($ugt.err[code] || $ugt.err_generic);
|
||||
}, complete: function () { $ugp.renderLst(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
let $$ugp = { init2: $ugp.init2, auth: {} };
|
||||
export default $$ugp;
|
||||
@@ -0,0 +1,58 @@
|
||||
.ugpfrm {
|
||||
padding: 1.5rem 2rem;
|
||||
|
||||
.ugp_grid {
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 0.5rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(50, 50, 50, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.ugp_row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
padding: 0.55rem 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.ugp_row_hd {
|
||||
background: $fuchs_blau;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.ugp_row:not(.ugp_row_hd) {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease-in-out;
|
||||
|
||||
&:hover { background-color: #f3f6fb; }
|
||||
|
||||
&.selected {
|
||||
background-color: rgba($fuchs_akzent, 0.12);
|
||||
box-shadow: inset 3px 0 0 $fuchs_akzent;
|
||||
}
|
||||
}
|
||||
|
||||
.ugp_col_name { font-weight: 600; color: #222; }
|
||||
|
||||
.ugp_empty {
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.ugp_err {
|
||||
padding: 1rem 2rem;
|
||||
color: #b00020;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
let $ugt = {
|
||||
mdl: 'Gruppenverwaltung'
|
||||
, rel: 'Neu Laden'
|
||||
, col_name: 'Name'
|
||||
, loadfail: 'Die Gruppenliste konnte nicht geladen werden.'
|
||||
, empty: 'Es sind keine Benutzergruppen vorhanden.'
|
||||
, newgroup: 'Neue Gruppe'
|
||||
, newgroup_btn: 'Anlegen'
|
||||
, name_field: 'Gruppenname'
|
||||
, rename: 'Gruppe umbenennen'
|
||||
, rename_btn: 'Speichern'
|
||||
, delete: 'Gruppe löschen'
|
||||
, delete_confirm: 'Soll die Gruppe "{0}" wirklich gelöscht werden? Die Mitgliedschaft aller Benutzer in dieser Gruppe geht dabei verloren.'
|
||||
, err: {
|
||||
save_failed: 'Die Gruppe konnte nicht gespeichert werden.',
|
||||
delete_failed: 'Die Gruppe konnte nicht gelöscht werden.'
|
||||
}
|
||||
, err_generic: 'Die Änderung konnte nicht gespeichert werden.'
|
||||
};
|
||||
@@ -2845,6 +2845,7 @@ $.extend($t, {
|
||||
, m_admin: 'Administration'
|
||||
, m_admin_status: 'Systemüberwachung'
|
||||
, m_usr: 'Benutzerverwaltung'
|
||||
, m_usrgrp: 'Gruppenverwaltung'
|
||||
, rsp: 'Passwort ändern'
|
||||
, pnm: 'Die Passwörter stimmen nicht überein'
|
||||
, cps: 'Das neue Passwort wurde gespeichert.'
|
||||
@@ -3241,10 +3242,10 @@ $fis.draft = {
|
||||
})();
|
||||
|
||||
/* "Administration" is a single sidemenu dropdown (ocmsmenu supports nested items via `itm`)
|
||||
containing two buttons — Benutzerverwaltung (fn=usr) and Systemüberwachung (fn=admin) — both
|
||||
restricted to users whose fds_sys authorization exceeds 4. One auth check gates the whole
|
||||
dropdown; it (and therefore both module scripts) is only added after that check resolves, so
|
||||
lower-privileged users never see it or fetch either module's code.
|
||||
containing three buttons — Benutzerverwaltung (fn=usr), Gruppenverwaltung (fn=usrgrp) and
|
||||
Systemüberwachung (fn=admin) — all restricted to users whose fds_sys authorization exceeds 4.
|
||||
One auth check gates the whole dropdown; it (and therefore all module scripts) is only added
|
||||
after that check resolves, so lower-privileged users never see it or fetch any module's code.
|
||||
Called from fis_main_go.js once the DOM (and the initial main menu) is ready. */
|
||||
$fis.addAdminMenuButtonsIfAuthorized = function () {
|
||||
if ($('#m_admin_grp').length > 0) { return; } // idempotent
|
||||
@@ -3254,6 +3255,7 @@ $fis.addAdminMenuButtonsIfAuthorized = function () {
|
||||
lbl: $t.m_admin, id: 'm_admin_grp', ico: 'glyphicon glyphicon-cog',
|
||||
itm: [
|
||||
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' },
|
||||
{ lbl: $t.m_usrgrp, id: 'm_usrgrp', fnc: 'init:usrgrp', ico: 'glyphicon glyphicon-list' },
|
||||
{ lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard' }
|
||||
]
|
||||
});
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -28,7 +28,7 @@
|
||||
}
|
||||
.usrfrm .usr_row {
|
||||
display: grid;
|
||||
grid-template-columns: 2.75rem 1.6fr 1.8fr 8rem;
|
||||
grid-template-columns: 2.75rem 1.4fr 1.6fr 1.6fr 8rem;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.55rem 1rem;
|
||||
@@ -77,6 +77,16 @@
|
||||
color: #555;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.usrfrm .usr_col_groups {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.usrfrm .usr_row_hd .usr_col_groups {
|
||||
font-size: inherit;
|
||||
}
|
||||
.usrfrm .usr_pill {
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.6rem;
|
||||
@@ -110,7 +120,7 @@
|
||||
}
|
||||
.usrfrm .usr_row {
|
||||
grid-template-columns: 2.25rem 1fr;
|
||||
grid-template-areas: "avatar name" "avatar email" "avatar status";
|
||||
grid-template-areas: "avatar name" "avatar email" "avatar groups" "avatar status";
|
||||
}
|
||||
.usrfrm .usr_row_hd {
|
||||
display: none;
|
||||
@@ -125,6 +135,11 @@
|
||||
grid-area: email;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.usrfrm .usr_col_groups {
|
||||
grid-area: groups;
|
||||
font-size: 0.85rem;
|
||||
white-space: normal;
|
||||
}
|
||||
.usrfrm .usr_col_status {
|
||||
grid-area: status;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ let $uct = {
|
||||
, rel: 'Neu Laden'
|
||||
, col_name: 'Name'
|
||||
, col_email: 'E-Mail'
|
||||
, col_groups: 'Gruppen'
|
||||
, col_status: 'Status'
|
||||
, active: 'Aktiv'
|
||||
, disabled: 'Deaktiviert'
|
||||
@@ -75,6 +76,40 @@ let $usr = {
|
||||
let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0);
|
||||
return (f + n).toUpperCase() || '?';
|
||||
},
|
||||
/* Builds one (detached) grid row for a user record; shared by renderLst and refreshRow. */
|
||||
buildRow: function (rw) {
|
||||
let disabled = bool(rw.disabled, false) === true;
|
||||
let row = $$.dc('usr_row', { id: 'usr' + rw.useraccount_id });
|
||||
$$.dc('usr_avatar', row).text($usr.initials(rw.firstname, rw.name));
|
||||
$$.dc('usr_col_name', row).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
|
||||
$$.dc('usr_col_email', row).text(rw.email || '');
|
||||
$$.dc('usr_col_groups', row).text(rw.groups || '').attr('title', rw.groups || '');
|
||||
$$.dc('usr_col_status', row).append(
|
||||
$$.dc(disabled ? 'usr_pill usr_pill_off' : 'usr_pill usr_pill_on').text(disabled ? $uct.disabled : $uct.active)
|
||||
);
|
||||
row.click(function () {
|
||||
row.toggleClass('selected').siblings().rC('selected');
|
||||
if (row.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); }
|
||||
});
|
||||
return row;
|
||||
},
|
||||
/* Re-fetches a single user and swaps its grid row in place, keeping selection + ribbon
|
||||
(used after changes that only affect that one row: email, group membership). Falls back
|
||||
to a full reload when the row is no longer in the grid. */
|
||||
refreshRow: function (rw) {
|
||||
let old = $('#usr' + rw.useraccount_id);
|
||||
if (old.length === 0) { $usr.renderLst(); return; }
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usr/list'), data: { useraccount_id: rw.useraccount_id }, success: function (r) {
|
||||
let fresh = (r.users || [])[0];
|
||||
if (!fresh) { $usr.renderLst(); return; }
|
||||
let selected = old.is('.selected');
|
||||
let row = $usr.buildRow(fresh).toggleClass('selected', selected);
|
||||
old.replaceWith(row);
|
||||
if (selected) { $usr.iMn(fresh); }
|
||||
}, error: function () { $usr.renderLst(); }
|
||||
});
|
||||
},
|
||||
renderLst: function () {
|
||||
let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst);
|
||||
$usr.eM(true);
|
||||
@@ -86,25 +121,13 @@ let $usr = {
|
||||
$$.dc('usr_col_avatar', hdr);
|
||||
$$.dc('usr_col_name', hdr).text($uct.col_name);
|
||||
$$.dc('usr_col_email', hdr).text($uct.col_email);
|
||||
$$.dc('usr_col_groups', hdr).text($uct.col_groups);
|
||||
$$.dc('usr_col_status', hdr).text($uct.col_status);
|
||||
if ((r.users || []).length === 0) {
|
||||
$$.dc('usr_empty', grid).text($uct.empty);
|
||||
return;
|
||||
}
|
||||
$.each(r.users || [], function (ri, rw) {
|
||||
let disabled = bool(rw.disabled, false) === true;
|
||||
let row = $$.dc('usr_row', grid, { id: 'usr' + rw.useraccount_id });
|
||||
$$.dc('usr_avatar', row).text($usr.initials(rw.firstname, rw.name));
|
||||
$$.dc('usr_col_name', row).text(`${rw.firstname || ''} ${rw.name || ''}`.trim());
|
||||
$$.dc('usr_col_email', row).text(rw.email || '');
|
||||
$$.dc('usr_col_status', row).append(
|
||||
$$.dc(disabled ? 'usr_pill usr_pill_off' : 'usr_pill usr_pill_on').text(disabled ? $uct.disabled : $uct.active)
|
||||
);
|
||||
row.click(function () {
|
||||
row.toggleClass('selected').siblings().rC('selected');
|
||||
if (row.is('.selected') === true) { $usr.iMn(rw); } else { $usr.eM(true); }
|
||||
});
|
||||
});
|
||||
$.each(r.users || [], function (ri, rw) { grid.append($usr.buildRow(rw)); });
|
||||
}, error: function () {
|
||||
usrfrm.empty();
|
||||
$$.dc('usr_err', usrfrm).text($uct.loadfail);
|
||||
@@ -187,7 +210,7 @@ let $usr = {
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($uct.err[code] || $uct.err_generic);
|
||||
}, complete: function () { c.ldng(0); $usr.renderLst(); }
|
||||
}, complete: function () { c.ldng(0); $usr.refreshRow(rw); }
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -206,6 +229,7 @@ let $usr = {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usr/groupset'),
|
||||
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked },
|
||||
success: function () { $usr.refreshRow(rw); }, /* keep the groups csv in the grid current per toggle */
|
||||
error: function () {
|
||||
cb.prop('checked', !checked);
|
||||
alert($uct.err_generic);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
.usrfrm{padding:1.5rem 2rem}.usrfrm .usr_grid{border:1px solid #e2e2e2;border-radius:.5rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.1);overflow:hidden}.usrfrm .usr_row{display:grid;grid-template-columns:2.75rem 1.6fr 1.8fr 8rem;align-items:center;gap:.75rem;padding:.55rem 1rem;border-bottom:1px solid #eee}.usrfrm .usr_row:last-child{border-bottom:none}.usrfrm .usr_row_hd{background:#1b4379;color:#fff;font-weight:700;font-size:.85rem;text-transform:uppercase;letter-spacing:.02em;padding:.5rem 1rem}.usrfrm .usr_row:not(.usr_row_hd){cursor:pointer;transition:background-color .12s ease-in-out}.usrfrm .usr_row:not(.usr_row_hd):hover{background-color:#f3f6fb}.usrfrm .usr_row:not(.usr_row_hd).selected{background-color:rgba(86,165,50,.12);box-shadow:inset 3px 0 0 #56a532}.usrfrm .usr_avatar{width:2.1rem;height:2.1rem;border-radius:50%;background:#1b4379;color:#fff;font-size:.78rem;font-weight:700;display:flex;align-items:center;justify-content:center}.usrfrm .usr_col_name{font-weight:600;color:#222}.usrfrm .usr_col_email{color:#555;overflow-wrap:anywhere}.usrfrm .usr_pill{display:inline-block;padding:.1rem .6rem;border-radius:1rem;font-size:.78rem;font-weight:700;white-space:nowrap}.usrfrm .usr_pill_on{background:#e2f3da;color:#2f6d18}.usrfrm .usr_pill_off{background:#ececec;color:#777}.usrfrm .usr_empty{padding:1.5rem 1rem;text-align:center;color:#777}.usrfrm .usr_err{padding:1rem 2rem;color:#b00020;font-weight:700}@media (max-width:640px){.usrfrm{padding:.75rem}.usrfrm .usr_row{grid-template-columns:2.25rem 1fr;grid-template-areas:"avatar name" "avatar email" "avatar status"}.usrfrm .usr_row_hd{display:none}.usrfrm .usr_avatar{grid-area:avatar}.usrfrm .usr_col_name{grid-area:name}.usrfrm .usr_col_email{grid-area:email;font-size:.85rem}.usrfrm .usr_col_status{grid-area:status}}.usr_groups_dlg{padding:.25rem 0}.usr_groups_dlg .usr_group_row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid #eee}.usr_groups_dlg .usr_group_row:last-child{border-bottom:none}.usr_groups_dlg .usr_group_row input[type=checkbox]{margin:0}.usr_groups_dlg .usr_group_row label{margin:0;font-weight:400}
|
||||
.usrfrm{padding:1.5rem 2rem}.usrfrm .usr_grid{border:1px solid #e2e2e2;border-radius:.5rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.1);overflow:hidden}.usrfrm .usr_row{display:grid;grid-template-columns:2.75rem 1.4fr 1.6fr 1.6fr 8rem;align-items:center;gap:.75rem;padding:.55rem 1rem;border-bottom:1px solid #eee}.usrfrm .usr_row:last-child{border-bottom:none}.usrfrm .usr_row_hd{background:#1b4379;color:#fff;font-weight:700;font-size:.85rem;text-transform:uppercase;letter-spacing:.02em;padding:.5rem 1rem}.usrfrm .usr_row:not(.usr_row_hd){cursor:pointer;transition:background-color .12s ease-in-out}.usrfrm .usr_row:not(.usr_row_hd):hover{background-color:#f3f6fb}.usrfrm .usr_row:not(.usr_row_hd).selected{background-color:rgba(86,165,50,.12);box-shadow:inset 3px 0 0 #56a532}.usrfrm .usr_avatar{width:2.1rem;height:2.1rem;border-radius:50%;background:#1b4379;color:#fff;font-size:.78rem;font-weight:700;display:flex;align-items:center;justify-content:center}.usrfrm .usr_col_name{font-weight:600;color:#222}.usrfrm .usr_col_email{color:#555;overflow-wrap:anywhere}.usrfrm .usr_col_groups{color:#555;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.usrfrm .usr_row_hd .usr_col_groups{font-size:inherit}.usrfrm .usr_pill{display:inline-block;padding:.1rem .6rem;border-radius:1rem;font-size:.78rem;font-weight:700;white-space:nowrap}.usrfrm .usr_pill_on{background:#e2f3da;color:#2f6d18}.usrfrm .usr_pill_off{background:#ececec;color:#777}.usrfrm .usr_empty{padding:1.5rem 1rem;text-align:center;color:#777}.usrfrm .usr_err{padding:1rem 2rem;color:#b00020;font-weight:700}@media (max-width:640px){.usrfrm{padding:.75rem}.usrfrm .usr_row{grid-template-columns:2.25rem 1fr;grid-template-areas:"avatar name" "avatar email" "avatar groups" "avatar status"}.usrfrm .usr_row_hd{display:none}.usrfrm .usr_avatar{grid-area:avatar}.usrfrm .usr_col_name{grid-area:name}.usrfrm .usr_col_email{grid-area:email;font-size:.85rem}.usrfrm .usr_col_groups{grid-area:groups;font-size:.85rem;white-space:normal}.usrfrm .usr_col_status{grid-area:status}}.usr_groups_dlg{padding:.25rem 0}.usr_groups_dlg .usr_group_row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid #eee}.usr_groups_dlg .usr_group_row:last-child{border-bottom:none}.usr_groups_dlg .usr_group_row input[type=checkbox]{margin:0}.usr_groups_dlg .usr_group_row label{margin:0;font-weight:400}
|
||||
@@ -0,0 +1,73 @@
|
||||
/* basics */
|
||||
/* Color SCHEME */
|
||||
/* #8396bd */
|
||||
/* #e3e6e6 */
|
||||
/* #ffffff */
|
||||
/* #262626 */
|
||||
/* #262626 */
|
||||
/* #e3e6e6 */
|
||||
/* #FFC801 */
|
||||
/* #ffff4a */
|
||||
/* media breaks */
|
||||
/* other */
|
||||
/* mixin */
|
||||
/* basics */
|
||||
/* Color SCHEME */
|
||||
/* #1b4379 #0033b3 */
|
||||
/*neu: #56a532 ; alt: rgb(32,144,119);*/
|
||||
/* 10% sw */
|
||||
.ugpfrm {
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
.ugpfrm .ugp_grid {
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 0.5rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(50, 50, 50, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 32rem;
|
||||
}
|
||||
.ugpfrm .ugp_row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
padding: 0.55rem 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.ugpfrm .ugp_row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.ugpfrm .ugp_row_hd {
|
||||
background: rgb(27, 67, 121);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.ugpfrm .ugp_row:not(.ugp_row_hd) {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease-in-out;
|
||||
}
|
||||
.ugpfrm .ugp_row:not(.ugp_row_hd):hover {
|
||||
background-color: #f3f6fb;
|
||||
}
|
||||
.ugpfrm .ugp_row:not(.ugp_row_hd).selected {
|
||||
background-color: rgba(86, 165, 50, 0.12);
|
||||
box-shadow: inset 3px 0 0 rgb(86, 165, 50);
|
||||
}
|
||||
.ugpfrm .ugp_col_name {
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
.ugpfrm .ugp_empty {
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
}
|
||||
.ugpfrm .ugp_err {
|
||||
padding: 1rem 2rem;
|
||||
color: #b00020;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
let $ugt = {
|
||||
mdl: 'Gruppenverwaltung'
|
||||
, rel: 'Neu Laden'
|
||||
, col_name: 'Name'
|
||||
, loadfail: 'Die Gruppenliste konnte nicht geladen werden.'
|
||||
, empty: 'Es sind keine Benutzergruppen vorhanden.'
|
||||
, newgroup: 'Neue Gruppe'
|
||||
, newgroup_btn: 'Anlegen'
|
||||
, name_field: 'Gruppenname'
|
||||
, rename: 'Gruppe umbenennen'
|
||||
, rename_btn: 'Speichern'
|
||||
, delete: 'Gruppe löschen'
|
||||
, delete_confirm: 'Soll die Gruppe "{0}" wirklich gelöscht werden? Die Mitgliedschaft aller Benutzer in dieser Gruppe geht dabei verloren.'
|
||||
, err: {
|
||||
save_failed: 'Die Gruppe konnte nicht gespeichert werden.',
|
||||
delete_failed: 'Die Gruppe konnte nicht gelöscht werden.'
|
||||
}
|
||||
, err_generic: 'Die Änderung konnte nicht gespeichert werden.'
|
||||
};
|
||||
|
||||
let $ugp = {
|
||||
init2: function () {
|
||||
$ocms.getScript([], function () { $ugp.init3(); });
|
||||
},
|
||||
init3: function () {
|
||||
$fis.cf(true);
|
||||
$fis.lf(true);
|
||||
$('#activemodule').text($ugt.mdl);
|
||||
$ugp.eM();
|
||||
$ugp.renderLst();
|
||||
},
|
||||
/* Default ribbon (nothing selected). */
|
||||
eM: function (r) {
|
||||
let m = [{ lbl: $ugt.newgroup_btn, fnc: $ugp.createDlg }];
|
||||
if (bool(r, false) === true) { m.push({ lbl: $ugt.rel, fnc: $ugp.renderLst }); }
|
||||
return $('#topbar').ocmsmenu(m);
|
||||
},
|
||||
/* Per-row ribbon shown while a group row is selected. */
|
||||
iMn: function (rw) {
|
||||
return $('#topbar').ocmsmenu([
|
||||
{ lbl: $ugt.rename, fnc: function () { $ugp.renameDlg(rw); } },
|
||||
{ lbl: $ugt.delete, fnc: function () { $ugp.deleteGroup(rw); } }
|
||||
]);
|
||||
},
|
||||
renderLst: function () {
|
||||
let ugplst = $fis.frm_list(true, true).ldng(1), ugpfrm = $$.dc('ugpfrm', ugplst);
|
||||
$ugp.eM(true);
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/list'), success: function (r) {
|
||||
ugpfrm.empty();
|
||||
let grid = $$.dc('ugp_grid', ugpfrm);
|
||||
let hdr = $$.dc('ugp_row ugp_row_hd', grid);
|
||||
$$.dc('ugp_col_name', hdr).text($ugt.col_name);
|
||||
if ((r.groups || []).length === 0) {
|
||||
$$.dc('ugp_empty', grid).text($ugt.empty);
|
||||
return;
|
||||
}
|
||||
$.each(r.groups || [], function (gi, rw) {
|
||||
let row = $$.dc('ugp_row', grid, { id: 'ugp' + rw.usergroup_id });
|
||||
$$.dc('ugp_col_name', row).text(rw.name || '');
|
||||
row.click(function () {
|
||||
row.toggleClass('selected').siblings().rC('selected');
|
||||
if (row.is('.selected') === true) { $ugp.iMn(rw); } else { $ugp.eM(true); }
|
||||
});
|
||||
});
|
||||
}, error: function () {
|
||||
ugpfrm.empty();
|
||||
$$.dc('ugp_err', ugpfrm).text($ugt.loadfail);
|
||||
}, complete: function () { ugplst.ldng(0); }
|
||||
});
|
||||
},
|
||||
createDlg: function () {
|
||||
$ocms.dlgform([
|
||||
{ name: 'name', label: $ugt.name_field, type: 'string', required: true }
|
||||
], {
|
||||
title: $ugt.newgroup,
|
||||
button: $ugt.newgroup_btn,
|
||||
submit: function (e) {
|
||||
let c = $(this).ldng(1);
|
||||
let qs = c.serializeObject(true, { typedvalues: true });
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/create'), data: qs, success: function () {
|
||||
c.trigger('modal_close');
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($ugt.err[code] || $ugt.err_generic);
|
||||
}, complete: function () { c.ldng(0); $ugp.renderLst(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
renameDlg: function (rw) {
|
||||
$ocms.dlgform([
|
||||
{ name: 'usergroup_id', type: 'hidden', value: rw.usergroup_id },
|
||||
{ name: 'name', label: $ugt.name_field, type: 'string', required: true, value: rw.name }
|
||||
], {
|
||||
title: $ugt.rename,
|
||||
button: $ugt.rename_btn,
|
||||
submit: function (e) {
|
||||
let c = $(this).ldng(1);
|
||||
let qs = c.serializeObject(true, { typedvalues: true });
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/rename'), data: qs, success: function () {
|
||||
c.trigger('modal_close');
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($ugt.err[code] || $ugt.err_generic);
|
||||
}, complete: function () { c.ldng(0); $ugp.renderLst(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteGroup: function (rw) {
|
||||
if (confirm(string($ugt.delete_confirm, [rw.name || '']))) {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('usrgrp/delete'), data: { usergroup_id: rw.usergroup_id }, success: function () {
|
||||
$ugp.eM(true);
|
||||
}, error: function (jqXHR) {
|
||||
let code = ((jqXHR || {}).responseJSON || {}).error;
|
||||
alert($ugt.err[code] || $ugt.err_generic);
|
||||
}, complete: function () { $ugp.renderLst(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
let $$ugp = { init2: $ugp.init2, auth: {} };
|
||||
export default $$ugp;
|
||||
+1
@@ -0,0 +1 @@
|
||||
let $ugt={mdl:"Gruppenverwaltung",rel:"Neu Laden",col_name:"Name",loadfail:"Die Gruppenliste konnte nicht geladen werden.",empty:"Es sind keine Benutzergruppen vorhanden.",newgroup:"Neue Gruppe",newgroup_btn:"Anlegen",name_field:"Gruppenname",rename:"Gruppe umbenennen",rename_btn:"Speichern",delete:"Gruppe löschen",delete_confirm:'Soll die Gruppe "{0}" wirklich gelöscht werden? Die Mitgliedschaft aller Benutzer in dieser Gruppe geht dabei verloren.',err:{save_failed:"Die Gruppe konnte nicht gespeichert werden.",delete_failed:"Die Gruppe konnte nicht gelöscht werden."},err_generic:"Die Änderung konnte nicht gespeichert werden."},$ugp={init2:function(){$ocms.getScript([],(function(){$ugp.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($ugt.mdl),$ugp.eM(),$ugp.renderLst()},eM:function(e){let n=[{lbl:$ugt.newgroup_btn,fnc:$ugp.createDlg}];return!0===bool(e,!1)&&n.push({lbl:$ugt.rel,fnc:$ugp.renderLst}),$("#topbar").ocmsmenu(n)},iMn:function(e){return $("#topbar").ocmsmenu([{lbl:$ugt.rename,fnc:function(){$ugp.renameDlg(e)}},{lbl:$ugt.delete,fnc:function(){$ugp.deleteGroup(e)}}])},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),n=$$.dc("ugpfrm",e);$ugp.eM(!0),$ocms.postXT({url:$ocms.url("usrgrp/list"),success:function(e){n.empty();let t=$$.dc("ugp_grid",n),r=$$.dc("ugp_row ugp_row_hd",t);$$.dc("ugp_col_name",r).text($ugt.col_name),0!==(e.groups||[]).length?$.each(e.groups||[],(function(e,n){let r=$$.dc("ugp_row",t,{id:"ugp"+n.usergroup_id});$$.dc("ugp_col_name",r).text(n.name||""),r.click((function(){r.toggleClass("selected").siblings().rC("selected"),!0===r.is(".selected")?$ugp.iMn(n):$ugp.eM(!0)}))})):$$.dc("ugp_empty",t).text($ugt.empty)},error:function(){n.empty(),$$.dc("ugp_err",n).text($ugt.loadfail)},complete:function(){e.ldng(0)}})},createDlg:function(){$ocms.dlgform([{name:"name",label:$ugt.name_field,type:"string",required:!0}],{title:$ugt.newgroup,button:$ugt.newgroup_btn,submit:function(e){let n=$(this).ldng(1),t=n.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usrgrp/create"),data:t,success:function(){n.trigger("modal_close")},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){n.ldng(0),$ugp.renderLst()}})}})},renameDlg:function(e){$ocms.dlgform([{name:"usergroup_id",type:"hidden",value:e.usergroup_id},{name:"name",label:$ugt.name_field,type:"string",required:!0,value:e.name}],{title:$ugt.rename,button:$ugt.rename_btn,submit:function(e){let n=$(this).ldng(1),t=n.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usrgrp/rename"),data:t,success:function(){n.trigger("modal_close")},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){n.ldng(0),$ugp.renderLst()}})}})},deleteGroup:function(e){confirm(string($ugt.delete_confirm,[e.name||""]))&&$ocms.postXT({url:$ocms.url("usrgrp/delete"),data:{usergroup_id:e.usergroup_id},success:function(){$ugp.eM(!0)},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){$ugp.renderLst()}})}},$$ugp={init2:$ugp.init2,auth:{}};export default $$ugp;
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
.ugpfrm{padding:1.5rem 2rem}.ugpfrm .ugp_grid{border:1px solid #e2e2e2;border-radius:.5rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.1);overflow:hidden;max-width:32rem}.ugpfrm .ugp_row{display:grid;grid-template-columns:1fr;align-items:center;padding:.55rem 1rem;border-bottom:1px solid #eee}.ugpfrm .ugp_row:last-child{border-bottom:none}.ugpfrm .ugp_row_hd{background:#1b4379;color:#fff;font-weight:700;font-size:.85rem;text-transform:uppercase;letter-spacing:.02em;padding:.5rem 1rem}.ugpfrm .ugp_row:not(.ugp_row_hd){cursor:pointer;transition:background-color .12s ease-in-out}.ugpfrm .ugp_row:not(.ugp_row_hd):hover{background-color:#f3f6fb}.ugpfrm .ugp_row:not(.ugp_row_hd).selected{background-color:rgba(86,165,50,.12);box-shadow:inset 3px 0 0 #56a532}.ugpfrm .ugp_col_name{font-weight:600;color:#222}.ugpfrm .ugp_empty{padding:1.5rem 1rem;text-align:center;color:#777}.ugpfrm .ugp_err{padding:1rem 2rem;color:#b00020;font-weight:700}
|
||||
Reference in New Issue
Block a user