Compare commits

...
3 Commits
Author SHA1 Message Date
Stefan 2f39440b05 chore: Update OCORE submodule to latest commit
Playwright Tests / test (push) Canceled after 0s
2026-09-16 09:42:46 +02:00
Stefan b26c24ef98 feat: Enhance user group management and transaction handling in stored procedures
- Improved user group deletion logic to accurately report success or failure.
- Updated user group renaming checks to prevent unintended inserts or updates.
- Added documentation on transaction blocking risks during full resets in stored procedures.
- Removed unnecessary login scripts from database project.
2026-09-16 09:42:32 +02:00
Stefan 8bcec409c5 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.
2026-09-16 09:35:31 +02:00
59 changed files with 1380 additions and 94 deletions
@@ -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.AspNetCore.Mvc;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using OCORE.SQL; using OCORE.SQL;
@@ -44,10 +44,16 @@ public partial class IntranetController
{ {
case "list": 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( 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, _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)); Security: DbSec, options: SqlOpt(fn, id, code));
return AdminJson(new { users = res.DataTable.toArrayofObjectDictionaries() }); return AdminJson(new { users = res.DataTable.toArrayofObjectDictionaries() });
} }
+1
View File
@@ -168,6 +168,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
"bam" => await Do_Process_Bankings(fn, id, code), "bam" => await Do_Process_Bankings(fn, id, code),
"admin" => await Do_Process_Admin(fn, id, code), "admin" => await Do_Process_Admin(fn, id, code),
"usr" => await Do_Process_AdminUsers(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), "auth" => await HandleAuth(fn, id, code),
"spwc" => await HandleSendPasswordCode(fn, id, code), "spwc" => await HandleSendPasswordCode(fn, id, code),
"spw" => await HandleSendPassword(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)
+18 -6
View File
@@ -1,6 +1,6 @@
--- ---
status: Active status: Active
lastUpdated: 2026-09-14 lastUpdated: 2026-09-16
applyTo: applyTo:
- "Fuchs/Controllers/IntranetController.AdminUsers.cs" - "Fuchs/Controllers/IntranetController.AdminUsers.cs"
- "Fuchs/js/intranet/modules/fis.usr*.js" - "Fuchs/js/intranet/modules/fis.usr*.js"
@@ -12,6 +12,7 @@ relatedDecisions:
- "0014-admin-user-management.md" - "0014-admin-user-management.md"
- "0015-split-admin-user-management-module.md" - "0015-split-admin-user-management-module.md"
- "0020-user-management-create-and-delete.md" - "0020-user-management-create-and-delete.md"
- "0021-user-group-management.md"
--- ---
# User Management module (`usr`) # User Management module (`usr`)
@@ -51,7 +52,8 @@ click "Benutzerverwaltung" → $ocms.init('usr')
POST /do/usr/auth → { manage, level } (manage>0 ⇒ authorized) POST /do/usr/auth → { manage, level } (manage>0 ⇒ authorized)
load /web/fis.usr.de.js + /web/fis.usr.css load /web/fis.usr.de.js + /web/fis.usr.css
$ocms.usr.init2() → init3() → renderLst() $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) 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 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 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`) ### Backend (`IntranetController.AdminUsers.cs`)
- `list` → `fis_admin_getUserAccountList(@authaccount, NULL, NULL, NULL)`, all non-deleted - `list` → `fis_admin_getUserAccountList(@authaccount, @useraccount_id, NULL, NULL)`, all non-deleted
accounts with decrypted name/email/disabled flag. 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 - `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 `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 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 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" }`. duplicate email (`THROW 60001` in the proc) surfaces as HTTP 409 `{ error: "duplicate_email" }`.
- `groups` → the new `fis_admin_getUserGroupMembership(@authaccount, @useraccount_id)` function - `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 - `groupset` → calls `fis_admin_addGroupMember`/`remGroupMember` depending on the posted
`member` flag — applied immediately per checkbox toggle, no separate save step. `member` flag — applied immediately per checkbox toggle, no separate save step.
- `create` → `fis_admin_createUserAccount(@authaccount, @name, @firstname, @title, @gender, - `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`) ### 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 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 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 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 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). 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.
@@ -0,0 +1,65 @@
---
status: Accepted
date: 2026-09-16
applyTo:
- "Fuchs_Database/dbo/Stored Procedures/mfr__updt__*.sql"
- "Fuchs_DataService/FdsMfrClient.cs"
supersededBy: ""
---
# 0022 — MFR sync transaction blocking during full resets is an accepted risk
## Context
Every `mfr__updt__*` stored procedure (25 of them, one per synced mfr entity table) wraps its
DELETE-stale-references + MERGE-upsert logic in a single explicit `BEGIN TRANSACTION` /
`COMMIT TRANSACTION`, with `SET XACT_ABORT ON` and a `TRY/CATCH` that rolls back and logs to
`fds__admin_dberrors` on failure. This gives all-or-nothing atomicity for one entity's sync
batch: either every stale side-table row is cleaned up and the target table is upserted, or
none of it is, so a mid-batch failure can never leave orphaned side-table rows alongside a
stale/missing target row.
`FdsMfrClient.cs`'s `CommandAfter` (which invokes the `mfr__updt__*` proc) and `CommandBefore`/
staging `SqlBulkCopy` both use a 600s command timeout (`MergeCommandTimeoutSeconds`), raised
from `SqlCommand`'s 30s default specifically so a **full reset** of a large entity table (not
the normal incremental sync, which only carries the rows that changed since the last run) has
room to complete. Before that timeout was raised, a slow full-reset batch would self-abort at
30s — the client cancellation would cancel the batch and roll back the open transaction —
capping how long the transaction (and the locks it holds on the target table and the affected
side tables) could stay open. With the timeout at 600s, a genuinely large full-reset batch can
now hold that same transaction, and therefore those same locks, for up to ten minutes, during
which other readers of the live `mfr__*` tables (e.g. intranet pages querying companies,
contacts, invoices, …) can block waiting for the write transaction to release its locks —
instead of blocking, at most, on one DELETE or one MERGE statement individually.
## Decision
Accept this as a known, documented risk rather than changing behavior. Full resets are rare
(a manual/administrative operation, not the routine incremental sync path) and are expected to
run outside of business hours. The atomicity the transaction provides — no orphaned side-table
rows left behind by a failed sync — is worth more than shortening the worst-case blocking
window for an operation this infrequent.
## Consequences
- A full reset of a large `mfr__*` entity table can block ordinary intranet reads of that
table (and the side tables `mfr__*PartnerSet`, `mfr__#customvalues`, `mfr__#locations`,
`mfr__comments`) for as long as that reset's DELETE+MERGE takes to execute, up to the 600s
command timeout.
- Anyone scheduling or manually triggering a full reset of a large entity table should do so
during low-traffic hours, same as any other maintenance operation with table-level locking.
- If full resets become frequent enough, or large enough, for this blocking window to matter in
practice, revisit this decision — options considered and rejected here (see below) remain
available.
## Alternatives considered
- **Drop the explicit transaction**, letting each DELETE/MERGE statement auto-commit
individually (the behavior before the transaction wrapping was added). Shortens the lock
window back to per-statement, but reintroduces the exact orphaned-data risk on mid-batch
failure that the transaction wrapping was added to prevent. Rejected — full-batch atomicity
was a deliberate improvement and giving it up trades a rare, low-traffic-hours blocking issue
for a correctness regression on every sync.
- **Enable Read Committed Snapshot Isolation (RCSI)** (`ALTER DATABASE ... SET
READ_COMMITTED_SNAPSHOT ON`) so readers use row versioning instead of blocking on the writer's
locks, keeping the transaction's atomicity intact while removing reader blocking entirely.
This is the architecturally cleanest fix, but it is a database-wide isolation behavior change
(affects every table, every query, tempdb version-store load) that needs its own testing and
sign-off rather than being folded into this change. Left as the preferred follow-up if the
accepted risk above turns out not to be acceptable in practice.
+19
View File
@@ -191,5 +191,24 @@
"js/intranet/modules/fis.usr.scss" "js/intranet/modules/fis.usr.scss"
], ],
"minify": { "enabled": true } "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 }
} }
] ]
+5 -4
View File
@@ -12,10 +12,10 @@
})(); })();
/* "Administration" is a single sidemenu dropdown (ocmsmenu supports nested items via `itm`) /* "Administration" is a single sidemenu dropdown (ocmsmenu supports nested items via `itm`)
containing two buttons — Benutzerverwaltung (fn=usr) and Systemüberwachung (fn=admin) — both containing three buttons — Benutzerverwaltung (fn=usr), Gruppenverwaltung (fn=usrgrp) and
restricted to users whose fds_sys authorization exceeds 4. One auth check gates the whole Systemüberwachung (fn=admin) — all restricted to users whose fds_sys authorization exceeds 4.
dropdown; it (and therefore both module scripts) is only added after that check resolves, so One auth check gates the whole dropdown; it (and therefore all module scripts) is only added
lower-privileged users never see it or fetch either module's code. 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. */ Called from fis_main_go.js once the DOM (and the initial main menu) is ready. */
$fis.addAdminMenuButtonsIfAuthorized = function () { $fis.addAdminMenuButtonsIfAuthorized = function () {
if ($('#m_admin_grp').length > 0) { return; } // idempotent 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', lbl: $t.m_admin, id: 'm_admin_grp', ico: 'glyphicon glyphicon-cog',
itm: [ itm: [
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' }, { 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' } { lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard' }
] ]
}); });
+1
View File
@@ -7,6 +7,7 @@
, m_admin: 'Administration' , m_admin: 'Administration'
, m_admin_status: 'Systemüberwachung' , m_admin_status: 'Systemüberwachung'
, m_usr: 'Benutzerverwaltung' , m_usr: 'Benutzerverwaltung'
, m_usrgrp: 'Gruppenverwaltung'
, rsp: 'Passwort ändern' , rsp: 'Passwort ändern'
, pnm: 'Die Passwörter stimmen nicht überein' , pnm: 'Die Passwörter stimmen nicht überein'
, cps: 'Das neue Passwort wurde gespeichert.' , cps: 'Das neue Passwort wurde gespeichert.'
+42 -15
View File
@@ -30,6 +30,44 @@ let $usr = {
let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0); let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0);
return (f + n).toUpperCase() || '?'; 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. Looks up the current row node by
id inside the response handler (not before the request) so two overlapping calls — e.g.
two quick checkbox toggles in groupsDlg — each replace whatever row is live at the time
their own response arrives, instead of the second call operating on a node the first
already detached via replaceWith. */
refreshRow: function (rw) {
if ($('#usr' + rw.useraccount_id).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];
let old = $('#usr' + rw.useraccount_id);
if (!fresh || old.length === 0) { $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 () { renderLst: function () {
let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst); let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst);
$usr.eM(true); $usr.eM(true);
@@ -41,25 +79,13 @@ let $usr = {
$$.dc('usr_col_avatar', hdr); $$.dc('usr_col_avatar', hdr);
$$.dc('usr_col_name', hdr).text($uct.col_name); $$.dc('usr_col_name', hdr).text($uct.col_name);
$$.dc('usr_col_email', hdr).text($uct.col_email); $$.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); $$.dc('usr_col_status', hdr).text($uct.col_status);
if ((r.users || []).length === 0) { if ((r.users || []).length === 0) {
$$.dc('usr_empty', grid).text($uct.empty); $$.dc('usr_empty', grid).text($uct.empty);
return; return;
} }
$.each(r.users || [], function (ri, rw) { $.each(r.users || [], function (ri, rw) { grid.append($usr.buildRow(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); }
});
});
}, error: function () { }, error: function () {
usrfrm.empty(); usrfrm.empty();
$$.dc('usr_err', usrfrm).text($uct.loadfail); $$.dc('usr_err', usrfrm).text($uct.loadfail);
@@ -142,7 +168,7 @@ let $usr = {
}, error: function (jqXHR) { }, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error; let code = ((jqXHR || {}).responseJSON || {}).error;
alert($uct.err[code] || $uct.err_generic); alert($uct.err[code] || $uct.err_generic);
}, complete: function () { c.ldng(0); $usr.renderLst(); } }, complete: function () { c.ldng(0); $usr.refreshRow(rw); }
}); });
} }
}); });
@@ -161,6 +187,7 @@ let $usr = {
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/groupset'), url: $ocms.url('usr/groupset'),
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked }, 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 () { error: function () {
cb.prop('checked', !checked); cb.prop('checked', !checked);
alert($uct.err_generic); alert($uct.err_generic);
+5 -2
View File
@@ -11,7 +11,7 @@
.usr_row { .usr_row {
display: grid; 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; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.55rem 1rem; padding: 0.55rem 1rem;
@@ -57,6 +57,8 @@
.usr_col_name { font-weight: 600; color: #222; } .usr_col_name { font-weight: 600; color: #222; }
.usr_col_email { color: #555; overflow-wrap: anywhere; } .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 { .usr_pill {
display: inline-block; display: inline-block;
@@ -86,11 +88,12 @@
.usrfrm { .usrfrm {
padding: 0.75rem; 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_row_hd { display: none; }
.usr_avatar { grid-area: avatar; } .usr_avatar { grid-area: avatar; }
.usr_col_name { grid-area: name; } .usr_col_name { grid-area: name; }
.usr_col_email { grid-area: email; font-size: 0.85rem; } .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; } .usr_col_status { grid-area: status; }
} }
} }
@@ -3,6 +3,7 @@ let $uct = {
, rel: 'Neu Laden' , rel: 'Neu Laden'
, col_name: 'Name' , col_name: 'Name'
, col_email: 'E-Mail' , col_email: 'E-Mail'
, col_groups: 'Gruppen'
, col_status: 'Status' , col_status: 'Status'
, active: 'Aktiv' , active: 'Aktiv'
, disabled: 'Deaktiviert' , disabled: 'Deaktiviert'
+107
View File
@@ -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;
+58
View File
@@ -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.'
};
+6 -4
View File
@@ -2845,6 +2845,7 @@ $.extend($t, {
, m_admin: 'Administration' , m_admin: 'Administration'
, m_admin_status: 'Systemüberwachung' , m_admin_status: 'Systemüberwachung'
, m_usr: 'Benutzerverwaltung' , m_usr: 'Benutzerverwaltung'
, m_usrgrp: 'Gruppenverwaltung'
, rsp: 'Passwort ändern' , rsp: 'Passwort ändern'
, pnm: 'Die Passwörter stimmen nicht überein' , pnm: 'Die Passwörter stimmen nicht überein'
, cps: 'Das neue Passwort wurde gespeichert.' , cps: 'Das neue Passwort wurde gespeichert.'
@@ -3241,10 +3242,10 @@ $fis.draft = {
})(); })();
/* "Administration" is a single sidemenu dropdown (ocmsmenu supports nested items via `itm`) /* "Administration" is a single sidemenu dropdown (ocmsmenu supports nested items via `itm`)
containing two buttons — Benutzerverwaltung (fn=usr) and Systemüberwachung (fn=admin) — both containing three buttons — Benutzerverwaltung (fn=usr), Gruppenverwaltung (fn=usrgrp) and
restricted to users whose fds_sys authorization exceeds 4. One auth check gates the whole Systemüberwachung (fn=admin) — all restricted to users whose fds_sys authorization exceeds 4.
dropdown; it (and therefore both module scripts) is only added after that check resolves, so One auth check gates the whole dropdown; it (and therefore all module scripts) is only added
lower-privileged users never see it or fetch either module's code. 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. */ Called from fis_main_go.js once the DOM (and the initial main menu) is ready. */
$fis.addAdminMenuButtonsIfAuthorized = function () { $fis.addAdminMenuButtonsIfAuthorized = function () {
if ($('#m_admin_grp').length > 0) { return; } // idempotent 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', lbl: $t.m_admin, id: 'm_admin_grp', ico: 'glyphicon glyphicon-cog',
itm: [ itm: [
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' }, { 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' } { lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard' }
] ]
}); });
+1 -1
View File
File diff suppressed because one or more lines are too long
+17 -2
View File
@@ -28,7 +28,7 @@
} }
.usrfrm .usr_row { .usrfrm .usr_row {
display: grid; 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; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.55rem 1rem; padding: 0.55rem 1rem;
@@ -77,6 +77,16 @@
color: #555; color: #555;
overflow-wrap: anywhere; 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 { .usrfrm .usr_pill {
display: inline-block; display: inline-block;
padding: 0.1rem 0.6rem; padding: 0.1rem 0.6rem;
@@ -110,7 +120,7 @@
} }
.usrfrm .usr_row { .usrfrm .usr_row {
grid-template-columns: 2.25rem 1fr; 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 { .usrfrm .usr_row_hd {
display: none; display: none;
@@ -125,6 +135,11 @@
grid-area: email; grid-area: email;
font-size: 0.85rem; font-size: 0.85rem;
} }
.usrfrm .usr_col_groups {
grid-area: groups;
font-size: 0.85rem;
white-space: normal;
}
.usrfrm .usr_col_status { .usrfrm .usr_col_status {
grid-area: status; grid-area: status;
} }
+39 -15
View File
@@ -3,6 +3,7 @@ let $uct = {
, rel: 'Neu Laden' , rel: 'Neu Laden'
, col_name: 'Name' , col_name: 'Name'
, col_email: 'E-Mail' , col_email: 'E-Mail'
, col_groups: 'Gruppen'
, col_status: 'Status' , col_status: 'Status'
, active: 'Aktiv' , active: 'Aktiv'
, disabled: 'Deaktiviert' , disabled: 'Deaktiviert'
@@ -75,6 +76,40 @@ let $usr = {
let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0); let f = (firstname || '').trim().charAt(0), n = (name || '').trim().charAt(0);
return (f + n).toUpperCase() || '?'; 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 () { renderLst: function () {
let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst); let usrlst = $fis.frm_list(true, true).ldng(1), usrfrm = $$.dc('usrfrm', usrlst);
$usr.eM(true); $usr.eM(true);
@@ -86,25 +121,13 @@ let $usr = {
$$.dc('usr_col_avatar', hdr); $$.dc('usr_col_avatar', hdr);
$$.dc('usr_col_name', hdr).text($uct.col_name); $$.dc('usr_col_name', hdr).text($uct.col_name);
$$.dc('usr_col_email', hdr).text($uct.col_email); $$.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); $$.dc('usr_col_status', hdr).text($uct.col_status);
if ((r.users || []).length === 0) { if ((r.users || []).length === 0) {
$$.dc('usr_empty', grid).text($uct.empty); $$.dc('usr_empty', grid).text($uct.empty);
return; return;
} }
$.each(r.users || [], function (ri, rw) { $.each(r.users || [], function (ri, rw) { grid.append($usr.buildRow(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); }
});
});
}, error: function () { }, error: function () {
usrfrm.empty(); usrfrm.empty();
$$.dc('usr_err', usrfrm).text($uct.loadfail); $$.dc('usr_err', usrfrm).text($uct.loadfail);
@@ -187,7 +210,7 @@ let $usr = {
}, error: function (jqXHR) { }, error: function (jqXHR) {
let code = ((jqXHR || {}).responseJSON || {}).error; let code = ((jqXHR || {}).responseJSON || {}).error;
alert($uct.err[code] || $uct.err_generic); 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({ $ocms.postXT({
url: $ocms.url('usr/groupset'), url: $ocms.url('usr/groupset'),
data: { useraccount_id: rw.useraccount_id, usergroup_id: g.usergroup_id, member: checked }, 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 () { error: function () {
cb.prop('checked', !checked); cb.prop('checked', !checked);
alert($uct.err_generic); alert($uct.err_generic);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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}
+73
View File
@@ -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;
}
+127
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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}
+28 -3
View File
@@ -16,6 +16,12 @@ public class FdsMfrClient : IDisposable
private readonly MFRClient _mfrClient; private readonly MFRClient _mfrClient;
private readonly ILogger<FdsMfrClient> _logger; private readonly ILogger<FdsMfrClient> _logger;
// Full-reset merges of large entity tables exceed SqlCommand's 30 s default. Raising this
// also raises how long a full-reset's mfr__updt__* transaction can hold locks and block
// readers of the live mfr__* tables — an accepted, documented risk, see
// Docs/Decisions/0022-mfr-sync-transaction-blocking-accepted-risk.md.
private const int MergeCommandTimeoutSeconds = 600;
private static MFRClientCredentials DefaultCredentials() => private static MFRClientCredentials DefaultCredentials() =>
new(FdsConfig.MFR_UserName, FdsConfig.MFR_Password); new(FdsConfig.MFR_UserName, FdsConfig.MFR_Password);
@@ -256,12 +262,31 @@ public class FdsMfrClient : IDisposable
try try
{ {
tbl.AcceptChanges(); tbl.AcceptChanges();
var dtwa = new DatatableWriterAsync(tbl, FdsShared.FDSConnectionString()); var dtwa = new DatatableWriterAsync(tbl, FdsShared.FDSConnectionString())
dtwa.CommandAfterError = new SqlCommand($"SELECT * INTO [t_{setId.ToLower()}_{tbl.TableName}] FROM {dtwa.DestinationTableName};"); {
// A large full-reset batch can exceed SqlBulkCopy's 30s default just as
// easily as the merge proc it feeds into — match CommandAfter's timeout
// so staging itself doesn't fail before the merge/fds__setStatus ever runs.
BulkCopyTimeoutSeconds = MergeCommandTimeoutSeconds
};
// One fixed debug copy per entity table (last failed batch) instead of a new t_<setid>_ table per attempt.
string fallbackTable = $"t_err_{tbl.TableName}";
dtwa.CommandAfterError = new SqlCommand(
$"DROP TABLE IF EXISTS [{fallbackTable}]; SELECT * INTO [{fallbackTable}] FROM {dtwa.DestinationTableName};")
{ CommandTimeout = MergeCommandTimeoutSeconds };
dtwa.OnCommandAfterError += (sender, caex) =>
_logger.LogError(caex,
"Update__entitytable: merge into {Table} failed ({Entity}, setid={SetId}, rows={Rows}) — staged batch kept in {FallbackTable}",
tbl.TableName, thisEntityName, setId, trc, fallbackTable);
dtwa.OnError += (sender, exc, message) =>
_logger.LogError(exc,
"Update__entitytable: DatatableWriterAsync submission failed for {Table} ({Entity}, setid={SetId}) — {Message}",
tbl.TableName, thisEntityName, setId, message);
dtwa.CommandAfter = new SqlCommand( dtwa.CommandAfter = new SqlCommand(
$"EXECUTE [dbo].[{tbl.TableName.Replace("__", "__updt__")}] @tblname, @referencetable, @tgtid;" + $"EXECUTE [dbo].[{tbl.TableName.Replace("__", "__updt__")}] @tblname, @referencetable, @tgtid;" +
"EXECUTE [dbo].[fds__setStatus] @table,@action,@setid,@info;" + "EXECUTE [dbo].[fds__setStatus] @table,@action,@setid,@info;" +
(additionalCommandAfter ?? "")); (additionalCommandAfter ?? ""))
{ CommandTimeout = MergeCommandTimeoutSeconds };
dtwa.CommandAfter.Parameters.AddRange(new SqlParameter[] dtwa.CommandAfter.Parameters.AddRange(new SqlParameter[]
{ {
new("@tblname", dtwa.DestinationTableName), new("@tblname", dtwa.DestinationTableName),
@@ -15,7 +15,7 @@ CREATE FUNCTION [dbo].[fis_admin_getUserAccountList]
, @usergroup_id varchar(5) , @usergroup_id varchar(5)
, @include_nonmembers as bit , @include_nonmembers as bit
) )
RETURNS @RET TABLE ( [useraccount_id] varchar(5), [auth] smallint, [name] nvarchar(100) null, [firstname] nvarchar(100) null, [title] varchar(50) NULL, [gender] varchar(1), [email] varchar(255) NULL, [mobile] varchar(50) null, [functions] varchar(1000), [language] varchar(2), [disabled] bit, [nologin] bit, [member] bit, [order] int) RETURNS @RET TABLE ( [useraccount_id] varchar(5), [auth] smallint, [name] nvarchar(100) null, [firstname] nvarchar(100) null, [title] varchar(50) NULL, [gender] varchar(1), [email] varchar(255) NULL, [mobile] varchar(50) null, [functions] varchar(1000), [language] varchar(2), [disabled] bit, [nologin] bit, [member] bit, [groups] nvarchar(max) NULL, [order] int)
AS AS
BEGIN BEGIN
DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount); DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount);
@@ -49,6 +49,12 @@ BEGIN
,'disabled' = CASE WHEN ua.[DateDisabled] IS NULL THEN 0 ELSE 1 END ,'disabled' = CASE WHEN ua.[DateDisabled] IS NULL THEN 0 ELSE 1 END
,'nologin' = CAST(CASE WHEN ua.[password_enc] is null THEN 1 WHEN ISNULL(CAST(DECRYPTBYKEY(ua.[password_enc]) as varchar(20)),'') = '' THEN 1 ELSE 0 END as bit) ,'nologin' = CAST(CASE WHEN ua.[password_enc] is null THEN 1 WHEN ISNULL(CAST(DECRYPTBYKEY(ua.[password_enc]) as varchar(20)),'') = '' THEN 1 ELSE 0 END as bit)
,'member' = CASE WHEN @usergroup_id IS NULL THEN NULL WHEN sgm.[useraccount_id] is not null THEN 1 ELSE 0 END ,'member' = CASE WHEN @usergroup_id IS NULL THEN NULL WHEN sgm.[useraccount_id] is not null THEN 1 ELSE 0 END
,'groups' = (SELECT STRING_AGG(CAST(DECRYPTBYKEY(ug.[name_enc]) as nvarchar(max)), ', ') WITHIN GROUP (ORDER BY CAST(DECRYPTBYKEY(ug.[name_enc]) as nvarchar(255)))
FROM [dbo].[fis_usergroups_members] as ugm
JOIN [dbo].[fis_usergroups] as ug ON ug.[usergroup_id] = ugm.[usergroup_id]
WHERE ugm.[useraccount_id] = ua.[useraccount_id]
AND ugm.[DateDeleted] is null
AND ug.[DateDeleted] is null) /* csv of all active group memberships, alphabetically */
,'order' = ROW_NUMBER() OVER (ORDER BY CAST(DECRYPTBYKEY(ua.[name_enc]) as nvarchar(255))) ,'order' = ROW_NUMBER() OVER (ORDER BY CAST(DECRYPTBYKEY(ua.[name_enc]) as nvarchar(255)))
FROM [dbo].[fis_useraccounts] as ua FROM [dbo].[fis_useraccounts] as ua
LEFT JOIN SelectedGroupMembers as sgm on sgm.[useraccount_id] = ua.[useraccount_id] LEFT JOIN SelectedGroupMembers as sgm on sgm.[useraccount_id] = ua.[useraccount_id]
@@ -11,8 +11,9 @@ AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
DECLARE @utcnow datetime = GETUTCDATE(); DECLARE @utcnow datetime = GETUTCDATE();
DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount);
IF NOT EXISTS (SELECT * FROM [dbo].[fis_usergroups_members] WHERE [usergroup_id] = @usergroup_id AND [useraccount_id] = @useraccount_id and [DateDeleted] is null) IF @auth > 1 AND NOT EXISTS (SELECT * FROM [dbo].[fis_usergroups_members] WHERE [usergroup_id] = @usergroup_id AND [useraccount_id] = @useraccount_id and [DateDeleted] is null)
BEGIN BEGIN
INSERT INTO [dbo].[fis_usergroups_members] INSERT INTO [dbo].[fis_usergroups_members]
@@ -12,18 +12,36 @@ AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
DECLARE @utcnow datetime = GETUTCDATE(); DECLARE @utcnow datetime = GETUTCDATE();
DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount);
-- Tracks whether THIS call actually soft-deleted an active group, so a never-existing or
-- already-deleted @usergroup_id reports failure instead of a false "ok:true" (the previous
-- version derived success from "not currently active", which is also true when nothing here
-- ever existed).
DECLARE @deleted bit = 0;
IF EXISTS (SELECT * FROM [dbo].[fis_usergroups] WHERE [usergroup_id] = @usergroup_id and [DateDeleted] is null) IF @auth > 1 AND EXISTS (SELECT * FROM [dbo].[fis_usergroups] WHERE [usergroup_id] = @usergroup_id and [DateDeleted] is null)
BEGIN BEGIN
UPDATE [dbo].[fis_usergroups] UPDATE [dbo].[fis_usergroups]
SET [UserDeleted] = @authaccount SET [UserDeleted] = @authaccount
,[DateDeleted] = @utcnow ,[DateDeleted] = @utcnow
WHERE WHERE
[usergroup_id] = @usergroup_id; [usergroup_id] = @usergroup_id
AND [DateDeleted] is null;
IF @@ROWCOUNT > 0 SET @deleted = 1;
-- Soft-delete the group's active memberships too, matching the confirm text
-- ("Die Mitgliedschaft aller Benutzer in dieser Gruppe geht dabei verloren.").
UPDATE [dbo].[fis_usergroups_members]
SET [UserDeleted] = @authaccount
,[DateDeleted] = @utcnow
WHERE
[usergroup_id] = @usergroup_id
AND [DateDeleted] is null;
END END
SELECT CAST((CASE WHEN NOT EXISTS (SELECT * FROM [dbo].[fis_usergroups] WHERE [usergroup_id] = @usergroup_id and [DateDeleted] is null) THEN 1 ELSE 0 END) as bit); SELECT @deleted;
END END
@@ -12,8 +12,9 @@ AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
DECLARE @utcnow datetime = GETUTCDATE(); DECLARE @utcnow datetime = GETUTCDATE();
DECLARE @auth smallint = [dbo].[fis_admin_getUserAuth](@authaccount);
IF EXISTS (SELECT * FROM [dbo].[fis_usergroups_members] WHERE [usergroup_id] = @usergroup_id AND [useraccount_id] = @useraccount_id and [DateDeleted] is null) IF @auth > 1 AND EXISTS (SELECT * FROM [dbo].[fis_usergroups_members] WHERE [usergroup_id] = @usergroup_id AND [useraccount_id] = @useraccount_id and [DateDeleted] is null)
BEGIN BEGIN
UPDATE [dbo].[fis_usergroups_members] UPDATE [dbo].[fis_usergroups_members]
@@ -17,9 +17,14 @@ BEGIN
DECLARE @createdID as TABLE ([id] varchar(5)); DECLARE @createdID as TABLE ([id] varchar(5));
IF @auth > 1 -- Renaming an existing group (@usergroup_id supplied) requires that id to currently be an
-- active group; otherwise the MERGE below would silently INSERT it as a brand-new group
-- (rename-of-unknown-id becoming create) or resurrect/UPDATE a soft-deleted row without
-- clearing its DateDeleted (leaving it deleted yet renamed). @usergroup_id = NULL (create)
-- always passes this check.
IF @auth > 1 AND (@usergroup_id IS NULL OR EXISTS (SELECT * FROM [dbo].[fis_usergroups] WHERE [usergroup_id] = @usergroup_id AND [DateDeleted] IS NULL))
BEGIN BEGIN
MERGE [dbo].[fis_usergroups] as TARGET MERGE [dbo].[fis_usergroups] as TARGET
USING (VALUES( ISNULL(@usergroup_id, [dbo].[fis_fn_id] ('fis_usergroups')) USING (VALUES( ISNULL(@usergroup_id, [dbo].[fis_fn_id] ('fis_usergroups'))
,ENCRYPTBYKEY(@enc_key, @name) ,ENCRYPTBYKEY(@enc_key, @name)
@@ -11,12 +11,17 @@ CREATE PROCEDURE [dbo].[mfr__updt__#customvalues]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__#customvalues]; DECLARE @tmp [dbo].[mfr__tt__#customvalues];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM [' + @tblname + ']'; DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM [' + @tblname + ']';
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
--WITH tmp as ( --WITH tmp as (
-- SELECT *, ROW_NUMBER() OVER (PARTITION BY [id],[Property] ORDER BY [version] DESC) as '#' FROM @tmp -- SELECT *, ROW_NUMBER() OVER (PARTITION BY [id],[Property] ORDER BY [version] DESC) as '#' FROM @tmp
--), SOURCE as ( --), SOURCE as (
@@ -43,4 +48,15 @@ BEGIN
,SOURCE.[Property] ,SOURCE.[Property]
,SOURCE.[Meta] ,SOURCE.[Meta]
,SOURCE.[Value]); ,SOURCE.[Value]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,17 @@ CREATE PROCEDURE [dbo].[mfr__updt__#locations]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__#locations]; DECLARE @tmp [dbo].[mfr__tt__#locations];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM [' + @tblname + ']'; DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM [' + @tblname + ']';
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
WITH tmp as ( WITH tmp as (
SELECT *, ROW_NUMBER() OVER (PARTITION BY [id],[Property] ORDER BY [version] DESC) as '#' FROM @tmp SELECT *, ROW_NUMBER() OVER (PARTITION BY [id],[Property] ORDER BY [version] DESC) as '#' FROM @tmp
), SOURCE as ( ), SOURCE as (
@@ -64,4 +69,15 @@ BEGIN
,SOURCE.[Longitude] ,SOURCE.[Longitude]
,SOURCE.[Latitude] ,SOURCE.[Latitude]
,SOURCE.[IsValidLocation]); ,SOURCE.[IsValidLocation]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__*PartnerSet]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__*PartnerSet]; DECLARE @tmp [dbo].[mfr__tt__*PartnerSet];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM [' + @tblname + ']'; DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM [' + @tblname + ']';
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__*PartnerSet] as TARGET MERGE [dbo].[mfr__*PartnerSet] as TARGET
USING @tmp as SOURCE ON USING @tmp as SOURCE ON
@@ -40,4 +44,15 @@ BEGIN
,SOURCE.[PartnerType] ,SOURCE.[PartnerType]
,SOURCE.[PartnerId] ,SOURCE.[PartnerId]
,SOURCE.[EntityId]); ,SOURCE.[EntityId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__appointments]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__appointments]; DECLARE @tmp [dbo].[mfr__tt__appointments];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__appointments] as TARGET MERGE [dbo].[mfr__appointments] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -63,4 +67,15 @@ BEGIN
,SOURCE.[ServiceRequestId] ,SOURCE.[ServiceRequestId]
,SOURCE.[ContactId] ,SOURCE.[ContactId]
,SOURCE.[Note]); ,SOURCE.[Note]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__attachments]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__attachments]; DECLARE @tmp [dbo].[mfr__tt__attachments];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__attachments] as TARGET MERGE [dbo].[mfr__attachments] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -36,4 +40,15 @@ BEGIN
,SOURCE.[Version] ,SOURCE.[Version]
,SOURCE.[URI] ,SOURCE.[URI]
,SOURCE.[StepId]); ,SOURCE.[StepId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__comments]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__comments]; DECLARE @tmp [dbo].[mfr__tt__comments];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__comments] as TARGET MERGE [dbo].[mfr__comments] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -45,4 +49,15 @@ BEGIN
,SOURCE.[ServiceRequestId] ,SOURCE.[ServiceRequestId]
,SOURCE.[CompanyId] ,SOURCE.[CompanyId]
,SOURCE.[Content]); ,SOURCE.[Content]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,6 +10,9 @@ CREATE PROCEDURE [dbo].[mfr__updt__companies]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @now datetime = GETUTCDATE(); DECLARE @now datetime = GETUTCDATE();
DECLARE @tmp [dbo].[mfr__tt__companies]; DECLARE @tmp [dbo].[mfr__tt__companies];
@@ -17,6 +20,13 @@ BEGIN
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
-- Wrapping the DELETEs + MERGE below in one transaction gives all-or-nothing atomicity for
-- this entity's sync batch (same pattern in every mfr__updt__* proc). During a large full
-- reset this can hold locks on the affected tables for as long as the batch takes to run —
-- an accepted, documented risk, not an oversight. See
-- Docs/Decisions/0022-mfr-sync-transaction-blocking-accepted-risk.md before changing this.
BEGIN TRANSACTION;
IF @referencetable = 'mfr__companies' IF @referencetable = 'mfr__companies'
BEGIN BEGIN
DELETE p DELETE p
@@ -99,4 +109,15 @@ BEGIN
,SOURCE.[DateModified] ,SOURCE.[DateModified]
,SOURCE.[CustomValues] ,SOURCE.[CustomValues]
,SOURCE.[MainContactId]); ,SOURCE.[MainContactId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__contacts]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__contacts]; DECLARE @tmp [dbo].[mfr__tt__contacts];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__contacts] as TARGET MERGE [dbo].[mfr__contacts] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -75,4 +79,15 @@ BEGIN
,SOURCE.[ExternalId] ,SOURCE.[ExternalId]
,SOURCE.[Gender] ,SOURCE.[Gender]
,SOURCE.[UserId]); ,SOURCE.[UserId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__costcenters]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__costcenters]; DECLARE @tmp [dbo].[mfr__tt__costcenters];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__costcenters] as TARGET MERGE [dbo].[mfr__costcenters] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -33,4 +37,15 @@ BEGIN
(SOURCE.[Id] (SOURCE.[Id]
,SOURCE.[Version] ,SOURCE.[Version]
,SOURCE.[Name]); ,SOURCE.[Name]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__documents]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__documents]; DECLARE @tmp [dbo].[mfr__tt__documents];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__documents] as TARGET MERGE [dbo].[mfr__documents] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -75,4 +79,15 @@ BEGIN
,SOURCE.[HTMLCode] ,SOURCE.[HTMLCode]
,SOURCE.[ServiceRequestId] ,SOURCE.[ServiceRequestId]
,SOURCE.[Note]); ,SOURCE.[Note]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__invoices]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__invoices]; DECLARE @tmp [dbo].[mfr__tt__invoices];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__invoices] as TARGET MERGE [dbo].[mfr__invoices] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -80,4 +84,15 @@ BEGIN
,SOURCE.[InvoiceState] ,SOURCE.[InvoiceState]
,SOURCE.[Note] ,SOURCE.[Note]
,newid()); ,newid());
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__items]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__items]; DECLARE @tmp [dbo].[mfr__tt__items];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__items] as TARGET MERGE [dbo].[mfr__items] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -96,4 +100,15 @@ BEGIN
,SOURCE.[CreatorId] ,SOURCE.[CreatorId]
,SOURCE.[UnitId] ,SOURCE.[UnitId]
,SOURCE.[UnitString]); ,SOURCE.[UnitString]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__itemtypes]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__itemtypes]; DECLARE @tmp [dbo].[mfr__tt__itemtypes];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__itemtypes] as TARGET MERGE [dbo].[mfr__itemtypes] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -87,4 +91,15 @@ BEGIN
,SOURCE.[IsWarehouse] ,SOURCE.[IsWarehouse]
,SOURCE.[UnitId] ,SOURCE.[UnitId]
,SOURCE.[UnitString]); ,SOURCE.[UnitString]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__itemunits]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__itemunits]; DECLARE @tmp [dbo].[mfr__tt__itemunits];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__itemunits] as TARGET MERGE [dbo].[mfr__itemunits] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -33,4 +37,15 @@ BEGIN
(SOURCE.[Id] (SOURCE.[Id]
,SOURCE.[Version] ,SOURCE.[Version]
,SOURCE.[Name]); ,SOURCE.[Name]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__products]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__products]; DECLARE @tmp [dbo].[mfr__tt__products];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__products] as TARGET MERGE [dbo].[mfr__products] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -48,4 +52,15 @@ BEGIN
,SOURCE.[DateModified] ,SOURCE.[DateModified]
,SOURCE.[MappingId] ,SOURCE.[MappingId]
,SOURCE.[CustomValueStepTemplateId]); ,SOURCE.[CustomValueStepTemplateId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__qualifications]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__qualifications]; DECLARE @tmp [dbo].[mfr__tt__qualifications];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__qualifications] as TARGET MERGE [dbo].[mfr__qualifications] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -33,4 +37,15 @@ BEGIN
(SOURCE.[Id] (SOURCE.[Id]
,SOURCE.[Version] ,SOURCE.[Version]
,SOURCE.[Name]); ,SOURCE.[Name]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__reports]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__reports]; DECLARE @tmp [dbo].[mfr__tt__reports];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__reports] as TARGET MERGE [dbo].[mfr__reports] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -72,4 +76,15 @@ BEGIN
,SOURCE.[IsInvoice] ,SOURCE.[IsInvoice]
,SOURCE.[IsCanceled] ,SOURCE.[IsCanceled]
,SOURCE.[CustomValues]); ,SOURCE.[CustomValues]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,6 +10,9 @@ CREATE PROCEDURE [dbo].[mfr__updt__serviceobjects]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @now datetime = GETUTCDATE(); DECLARE @now datetime = GETUTCDATE();
DECLARE @tmp [dbo].[mfr__tt__serviceobjects]; DECLARE @tmp [dbo].[mfr__tt__serviceobjects];
@@ -17,6 +20,8 @@ BEGIN
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
IF @referencetable = 'mfr__serviceobjects' IF @referencetable = 'mfr__serviceobjects'
BEGIN BEGIN
DELETE p DELETE p
@@ -90,4 +95,15 @@ BEGIN
,SOURCE.[ParentServiceObjectId] ,SOURCE.[ParentServiceObjectId]
,SOURCE.[CompanyId] ,SOURCE.[CompanyId]
,SOURCE.[ProductId]); ,SOURCE.[ProductId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,6 +10,7 @@ CREATE PROCEDURE [dbo].[mfr__updt__servicerequests]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY BEGIN TRY
@@ -151,20 +152,11 @@ BEGIN
COMMIT TRANSACTION; COMMIT TRANSACTION;
END TRY END TRY
BEGIN CATCH BEGIN CATCH
INSERT INTO dbo.[fds__admin_dberrors] IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE()); VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
-- Transaction uncommittable
--IF (XACT_STATE()) = -1
ROLLBACK TRANSACTION;
-- Transaction committable
--IF (XACT_STATE()) = 1
--COMMIT TRANSACTION;
DECLARE @Message varchar(MAX) = ERROR_MESSAGE(), @Severity int = ERROR_SEVERITY(), @State smallint = ERROR_STATE();
RAISERROR (@Message, @Severity, @State);
THROW;
END CATCH; END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__steplisttemplateinstances]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__steplisttemplateinstances]; DECLARE @tmp [dbo].[mfr__tt__steplisttemplateinstances];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__steplisttemplateinstances] as TARGET MERGE [dbo].[mfr__steplisttemplateinstances] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -39,4 +43,15 @@ BEGIN
,SOURCE.[Name] ,SOURCE.[Name]
,SOURCE.[ServiceRequestId] ,SOURCE.[ServiceRequestId]
,SOURCE.[TemplateId]); ,SOURCE.[TemplateId]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,15 +10,19 @@ CREATE PROCEDURE [dbo].[mfr__updt__steplisttemplates]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @now datetime = GETUTCDATE(); DECLARE @now datetime = GETUTCDATE();
DECLARE @tmp [dbo].[mfr__tt__steplisttemplates]; DECLARE @tmp [dbo].[mfr__tt__steplisttemplates];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
IF @referencetable = 'mfr__steplisttemplates' IF @referencetable = 'mfr__steplisttemplates'
BEGIN BEGIN
DELETE stp DELETE stp
@@ -56,4 +60,15 @@ BEGIN
,SOURCE.[DateModified] ,SOURCE.[DateModified]
,SOURCE.[IsReleased] ,SOURCE.[IsReleased]
,SOURCE.[IsDurable]); ,SOURCE.[IsDurable]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,6 +10,9 @@ CREATE PROCEDURE [dbo].[mfr__updt__steps]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @now datetime = GETUTCDATE(); DECLARE @now datetime = GETUTCDATE();
DECLARE @tmp [dbo].[mfr__tt__steps]; DECLARE @tmp [dbo].[mfr__tt__steps];
@@ -53,6 +56,7 @@ BEGIN
END END
BEGIN TRANSACTION;
MERGE [dbo].[mfr__steps] as TARGET MERGE [dbo].[mfr__steps] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -146,4 +150,15 @@ BEGIN
-- ,[DateDeleted] = @now -- ,[DateDeleted] = @now
-- FROM @ARCHIVE -- FROM @ARCHIVE
-- WHERE [ActionType] IN ( 'DELETE', 'UPDATE' ); -- WHERE [ActionType] IN ( 'DELETE', 'UPDATE' );
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__stockmovements]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__stockmovements]; DECLARE @tmp [dbo].[mfr__tt__stockmovements];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__stockmovements] as TARGET MERGE [dbo].[mfr__stockmovements] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -72,4 +76,15 @@ BEGIN
,SOURCE.[TargetDate] ,SOURCE.[TargetDate]
,SOURCE.[CreatedById] ,SOURCE.[CreatedById]
,SOURCE.[ConfirmedById]); ,SOURCE.[ConfirmedById]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,17 @@ CREATE PROCEDURE [dbo].[mfr__updt__tags]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__tags]; DECLARE @tmp [dbo].[mfr__tt__tags];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
WITH tmp as ( WITH tmp as (
SELECT *, ROW_NUMBER() OVER (PARTITION BY [id] ORDER BY [version] DESC) as '#' FROM @tmp SELECT *, ROW_NUMBER() OVER (PARTITION BY [id] ORDER BY [version] DESC) as '#' FROM @tmp
), SOURCE as ( ), SOURCE as (
@@ -43,4 +48,15 @@ BEGIN
,SOURCE.[Name] ,SOURCE.[Name]
,SOURCE.[ColorDefinition] ,SOURCE.[ColorDefinition]
,SOURCE.[Type]); ,SOURCE.[Type]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,12 +10,16 @@ CREATE PROCEDURE [dbo].[mfr__updt__timeevents]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE @tmp [dbo].[mfr__tt__timeevents]; DECLARE @tmp [dbo].[mfr__tt__timeevents];
DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname DECLARE @tmp_cmd nvarchar(1000) = N'SELECT DISTINCT * FROM ' + @tblname
INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd; INSERT INTO @tmp EXECUTE [sp_executesql] @tmp_cmd;
BEGIN TRANSACTION;
MERGE [dbo].[mfr__timeevents] as TARGET MERGE [dbo].[mfr__timeevents] as TARGET
USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id] USING @tmp as SOURCE ON TARGET.[Id] = SOURCE.[Id]
@@ -63,4 +67,15 @@ BEGIN
,SOURCE.[ContactId] ,SOURCE.[ContactId]
,SOURCE.[ServiceRequestId] ,SOURCE.[ServiceRequestId]
,SOURCE.[Description]); ,SOURCE.[Description]);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
THROW;
END CATCH;
END END
@@ -10,6 +10,7 @@ CREATE PROCEDURE [dbo].[mfr__updt__users]
AS AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY BEGIN TRY
@@ -90,20 +91,11 @@ BEGIN
COMMIT TRANSACTION; COMMIT TRANSACTION;
END TRY END TRY
BEGIN CATCH BEGIN CATCH
INSERT INTO dbo.[fds__admin_dberrors] IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime])
VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE()); VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE());
-- Transaction uncommittable
--IF (XACT_STATE()) = -1
ROLLBACK TRANSACTION;
-- Transaction committable
--IF (XACT_STATE()) = 1
--COMMIT TRANSACTION;
DECLARE @Message varchar(MAX) = ERROR_MESSAGE(), @Severity int = ERROR_SEVERITY(), @State smallint = ERROR_STATE();
RAISERROR (@Message, @Severity, @State);
THROW;
END CATCH; END CATCH;
END END
+1 -1
Submodule OCORE updated: 0350e412bb...9d0e088ab3