Merge branch 'Workorder/improve-spacing' into 'master'
added new useredit page See merge request fronk/thetool!1735
This commit is contained in:
170
application/UserEdit/UserEditController.php
Normal file
170
application/UserEdit/UserEditController.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
class UserEditController extends mfBaseController {
|
||||
public User $user;
|
||||
private $postData;
|
||||
|
||||
protected function init() {
|
||||
$this->needlogin = true;
|
||||
$this->user = new User();
|
||||
$this->user->loadMe();
|
||||
$this->layout()->set('me', $this->user);
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
|
||||
// if post then set postData
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function indexAction() {
|
||||
$id = $this->request->id;
|
||||
if (!is_numeric($id) || $id <= 0) throw new Exception("User ID is required.", 400);
|
||||
|
||||
$user = new User($id);
|
||||
if (!$user->id) throw new Exception("User not found.", 404);
|
||||
|
||||
$preorderNetworks = $user->getFlag("preorder_networks")->value();
|
||||
$consentProjects = $user->getFlag("constructionConsent_projects")->value();
|
||||
|
||||
$userData = $user->toArray();
|
||||
$userData['permissions'] = (array)$user->permissions->data;
|
||||
$userData['preorder_networks'] = $preorderNetworks ? json_decode($preorderNetworks, true) : [];
|
||||
$userData['constructionconsent_projects'] = $consentProjects ? json_decode($consentProjects, true) : [];
|
||||
$userData['employee_number'] = $user->getFlag("employee_number")->value();
|
||||
$userData['project_api_key'] = $user->getFlag("project_api_key")->value();
|
||||
$userData['vodia_identity_domain'] = $user->getFlag("vodia_identity_domain")->value();
|
||||
$userData['vodia_identity_username'] = $user->getFlag("vodia_identity_username")->value();
|
||||
$userData['vodia_identity_default'] = $user->getFlag("vodia_identity_default")->value();
|
||||
|
||||
|
||||
$JS_VARIABLES = [
|
||||
"USER_DATA" => $userData,
|
||||
"LOOKUPS" => [
|
||||
"addresses" => array_map(fn($addr) => ['value' => $addr->id, 'text' => ($addr->company) ? $addr->company : $addr->getFullName()], AddressModel::getAll()),
|
||||
"networks" => array_map(fn($net) => ['value' => $net->id, 'text' => $net->name], NetworkModel::getAll()),
|
||||
"consentProjects" => array_map(fn($proj) => ['value' => $proj->id, 'text' => $proj->name], ConstructionConsentProject::getAll()),
|
||||
"permissionTemplates" => UserPermissionTemplateModel::getAll([], null, 0, ['key' => 'name', 'order' => 'asc']),
|
||||
"users" => array_map(fn($u) => ['value' => $u->id, 'text' => $u->name], UserModel::search(['active' => 1])),
|
||||
],
|
||||
"PERMISSIONS_CONFIG" => $this->getPermissionsConfig(),
|
||||
"SAVE_URL" => self::getUrl("User", "save"),
|
||||
"API_KEY_URL" => self::getUrl("User", "generateApikey"),
|
||||
];
|
||||
|
||||
Helper::renderVue($this, "UserEdit", "Benutzer bearbeiten: " . $user->name, $JS_VARIABLES);
|
||||
}
|
||||
|
||||
protected function getUserDataForTemplateAction() {
|
||||
$id = $this->request->id;
|
||||
if (!$id) self::sendError("User ID is required.");
|
||||
$user = new User($id);
|
||||
if (!$user->id) self::sendError("User not found.");
|
||||
|
||||
$preorderNetworks = $user->getFlag("preorder_networks")->value();
|
||||
$consentProjects = $user->getFlag("constructionConsent_projects")->value();
|
||||
|
||||
self::returnJson([
|
||||
'permissions' => (array)$user->permissions->data,
|
||||
'preorder_networks' => $preorderNetworks ? json_decode($preorderNetworks, true) : [],
|
||||
'constructionconsent_projects' => $consentProjects ? json_decode($consentProjects, true) : [],
|
||||
'vodia_identity_domain' => $user->getFlag("vodia_identity_domain")->value(),
|
||||
'vodia_identity_default' => $user->getFlag("vodia_identity_default")->value(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function managePermissionTemplatesAction() {
|
||||
Helper::renderVue($this, "UserPermissionTemplate", "Berechtigungsvorlagen", ["PERMISSIONS_CONFIG" => $this->getPermissionsConfig()]);
|
||||
}
|
||||
|
||||
protected function getPermissionTemplatesAction() {
|
||||
self::returnJson(array_map(
|
||||
function ($perm) {
|
||||
$perm = (array)$perm;
|
||||
$perm['permissions'] = json_decode($perm['permissions'], true) ?: [];
|
||||
return $perm;
|
||||
}, UserPermissionTemplateModel::getAll([], null, 0, ['key' => 'name', 'order' => 'asc'])
|
||||
));
|
||||
}
|
||||
|
||||
protected function savePermissionTemplateAction() {
|
||||
if (empty($this->postData['name'])) self::sendError("Template name is required.");
|
||||
|
||||
$data = [
|
||||
'name' => $this->postData['name'],
|
||||
'permissions' => json_encode($this->postData['permissions'] ?? []),
|
||||
];
|
||||
|
||||
if (empty($this->postData['id'])) {
|
||||
$data += ['createBy' => $this->user->id, 'create' => time()];
|
||||
$id = UserPermissionTemplateModel::create($data);
|
||||
self::returnJson(['success' => true, 'message' => 'Vorlage erstellt.', 'id' => $id]);
|
||||
}
|
||||
|
||||
$template = UserPermissionTemplateModel::get($this->postData['id']);
|
||||
$data += [
|
||||
'id' => $this->postData['id'],
|
||||
'create' => $template->create,
|
||||
'createBy' => $template->createBy,
|
||||
];
|
||||
|
||||
UserPermissionTemplateModel::update($data);
|
||||
self::returnJson(['success' => true, 'message' => 'Vorlage gespeichert.']);
|
||||
}
|
||||
|
||||
protected function deletePermissionTemplateAction() {
|
||||
$post = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($post['id'])) self::sendError("Template ID is required.");
|
||||
UserPermissionTemplateModel::delete($post['id']);
|
||||
self::returnJson(['success' => true, 'message' => 'Vorlage gelöscht.']);
|
||||
}
|
||||
|
||||
private function getPermissionsConfig(): array {
|
||||
return [
|
||||
'Rollen' => [
|
||||
'admin' => 'Administrator',
|
||||
'employee' => TT_SYSOWNER_NAME_HTML . ' Mitarbeiter',
|
||||
'technician' => 'Techniker',
|
||||
],
|
||||
'Preorder' => [
|
||||
'preorderfront' => 'Frontdesk (Semi-Readonly)',
|
||||
'preorderlogistics' => 'Logistikpartner',
|
||||
'preorderaddressreporting' => 'Address Reporting API User',
|
||||
'preorderreadonly' => 'Readonly',
|
||||
'canPreorder' => 'Modul: Vorbestellung',
|
||||
'canPreorderpricing' => 'Modul: Bepreisung',
|
||||
'canPreorderpricingReadonly' => 'Modul: Bepreisung (Readonly)',
|
||||
'canPreorderbilling' => 'Modul: Verrechnung',
|
||||
'canPreorderbillingReadonly' => 'Modul: Verrechnung (Readonly)',
|
||||
],
|
||||
'Module' => [
|
||||
'canBuilding' => 'Objekte & Anschlüsse',
|
||||
'canPipework' => 'Tiefbau',
|
||||
'canLinework' => 'Leitungsbau',
|
||||
'canPatching' => 'Patching',
|
||||
'canFilestore' => 'Filestore (Netzbau)',
|
||||
'canCpeprovisioning' => 'CPE Provisioning',
|
||||
'canCpeshipping' => 'CPE Versand',
|
||||
'canVoipnumbering' => 'VOIP Nummernverwaltung',
|
||||
'canOrder' => 'Bestellung',
|
||||
'canBilling' => 'Verrechnung',
|
||||
],
|
||||
'Lager' => [
|
||||
'canWarehouseAdmin' => 'Lager-Admin',
|
||||
'canWarehouseUser' => 'Lager-User',
|
||||
'canWarehouseEShop' => 'Energie Steiermark Shop',
|
||||
],
|
||||
'Zusatzberechtigungen' => [
|
||||
'canFibu' => 'Buchhaltung',
|
||||
'canStatistics' => 'Statistiken',
|
||||
'canADBExtended' => 'Address-DB erweitert',
|
||||
'canAssetAdmin' => 'Anlagen-Admin',
|
||||
'canRMLAdmin' => 'RML-Workorder-Admin',
|
||||
'canRMLCompany' => 'RML-Workorder-Firma',
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
class UserPermissionTemplateModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $name;
|
||||
public string $permissions; // JSON
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
|
||||
//SQL:
|
||||
// CREATE TABLE `UserPermissionTemplate` (
|
||||
// `id` int NOT NULL AUTO_INCREMENT,
|
||||
// `name` varchar(255) NOT NULL,
|
||||
// `permissions` text NOT NULL,
|
||||
// `create` int NOT NULL,
|
||||
// `createBy` int NOT NULL,
|
||||
// PRIMARY KEY (`id`)
|
||||
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
// ALTER TABLE `UserPermissionTemplate` ADD UNIQUE(`name`);
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateUserPermissionTemplate extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
if (!$this->hasTable('UserPermissionTemplate')) {
|
||||
$table = $this->table('UserPermissionTemplate', ['id' => false, 'primary_key' => ['id']]);
|
||||
$table->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 255, 'null' => false])
|
||||
->addColumn('permissions', 'text', ['null' => false])
|
||||
->addColumn('create', 'integer', ['null' => false])
|
||||
->addColumn('createBy', 'integer', ['null' => false])
|
||||
->addIndex(['name'], ['unique' => true])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
if ($this->hasTable('UserPermissionTemplate')) {
|
||||
$this->table('UserPermissionTemplate')->drop()->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
public/js/pages/UserEdit/UserEdit.css
Normal file
166
public/js/pages/UserEdit/UserEdit.css
Normal file
@@ -0,0 +1,166 @@
|
||||
.user-edit-container {
|
||||
padding-bottom: 60px; /* Space for the bottom save button */
|
||||
}
|
||||
|
||||
.user-edit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.user-edit-header.sticky-header {
|
||||
position: sticky;
|
||||
top: 60px; /* Adjust based on your main header height */
|
||||
background-color: #f8f9fa; /* Match card background */
|
||||
z-index: 10;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin: -1.25rem -1.25rem 1rem -1.25rem; /* Make it span the card width */
|
||||
}
|
||||
|
||||
.user-edit-header.collapsible {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 0.75rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.user-edit-header.collapsible:hover {
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
|
||||
.user-edit-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.user-form-grid-half {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.user-form-grid-toggles {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.user-form-grid-toggles .form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.permission-template-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.3rem;
|
||||
padding: 1rem;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.permission-group h5 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.password-generation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.selected-items-viewer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.selection-list-container {
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-top: 0.5rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.selection-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
.selection-list li:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.selection-list li .fa-times-circle {
|
||||
color: #dc3545;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.selection-list li .fa-times-circle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.user-edit-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0.75rem;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Slide-fade transition */
|
||||
.slide-fade-enter-active, .slide-fade-leave-active {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
.slide-fade-enter, .slide-fade-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
.slide-fade-fast-enter-active, .slide-fade-fast-leave-active {
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.slide-fade-fast-enter, .slide-fade-fast-leave-to {
|
||||
transform: translateY(-5px);
|
||||
opacity: 0;
|
||||
}
|
||||
350
public/js/pages/UserEdit/UserEdit.js
Normal file
350
public/js/pages/UserEdit/UserEdit.js
Normal file
@@ -0,0 +1,350 @@
|
||||
Vue.component("UserEdit", {
|
||||
template: `
|
||||
<tt-card>
|
||||
<div class="user-edit-container">
|
||||
<tt-loader v-if="isSaving"/>
|
||||
<div class="user-edit-header sticky-header">
|
||||
<h3>Allgemeine Informationen</h3>
|
||||
<tt-tooltip :text="permissionChangesTooltip" position="left">
|
||||
<tt-button text="Speichern" icon="fas fa-save" additional-class="btn-primary" @click="saveUser" :loading="isSaving"/>
|
||||
</tt-tooltip>
|
||||
</div>
|
||||
<div class="user-form-grid">
|
||||
<tt-input label="Username" v-model="user.username" sm/>
|
||||
<tt-input label="Name" v-model="user.name" sm/>
|
||||
<tt-input label="Email" v-model="user.email" type="email" sm/>
|
||||
<tt-input label="Handy Nr." v-model="user.mobile" placeholder="+43..." sm/>
|
||||
<tt-select label="Firma/Person" :options="lookups.addresses" v-model.number="user.address_id" sm/>
|
||||
</div>
|
||||
<div class="user-form-grid-toggles">
|
||||
<div class="form-group">
|
||||
<label>Aktiv</label>
|
||||
<tt-switch v-model="user.active" :loading="isToggling"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>2FA erzwingen</label>
|
||||
<tt-switch v-model="user.twofactorrequired" :loading="isToggling"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-edit-header collapsible" @click="toggleSection('permissions')">
|
||||
<h3>Berechtigungen</h3>
|
||||
<i :class="getChevronClass('permissions')"></i>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<tt-card v-show="!collapsedSections.permissions">
|
||||
<div class="permission-template-section">
|
||||
<tt-select label="Vorlage anwenden" :options="templateOptions" v-model="selectedTemplate" @input="applyTemplate" sm/>
|
||||
<tt-autocomplete label="Aus bestehenden User laden" :items="lookups.users" v-model="userToLoad" @input="loadDataFromUser" sm/>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="permissions-grid">
|
||||
<div v-for="(group, groupName) in permissionsConfig" :key="groupName" class="permission-group">
|
||||
<h5>{{ groupName }}</h5>
|
||||
<div v-for="(label, key) in group" :key="key" class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" :id="'perm-' + key" v-model="user.permissions[key]">
|
||||
<label class="form-check-label" :for="'perm-' + key" v-html="label"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
</transition>
|
||||
|
||||
<div v-if="user.permissions.employee">
|
||||
<div class="user-edit-header collapsible" @click="toggleSection('employeeSpecific')">
|
||||
<h3>Mitarbeiter-spezifische Felder</h3>
|
||||
<i :class="getChevronClass('employeeSpecific')"></i>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<tt-card v-show="!collapsedSections.employeeSpecific">
|
||||
<div class="user-form-grid">
|
||||
<tt-input label="Mitarbeiternummer" v-model="user.employee_number" sm/>
|
||||
<tt-input label="OpenProject API Key" v-model="user.project_api_key" sm/>
|
||||
<tt-input label="Vodia Domain" v-model="user.vodia_identity_domain" sm/>
|
||||
<tt-input label="Vodia Username (Extension)" v-model="user.vodia_identity_username" sm/>
|
||||
<tt-input label="Vodia Standard-Identität" v-model="user.vodia_identity_default" sm hint="+43720123456"/>
|
||||
</div>
|
||||
</tt-card>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="user-edit-header collapsible" @click="toggleSection('projects')">
|
||||
<h3>Projekt- & Netzwerkzugriff</h3>
|
||||
<i :class="getChevronClass('projects')"></i>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<tt-card v-show="!collapsedSections.projects">
|
||||
<div class="user-form-grid">
|
||||
<tt-select label="Preorder Netzgebiete" :options="lookups.networks" v-model="user.preorder_networks" multiple sm searchable/>
|
||||
<tt-select label="Zustimmungserklärungsprojekte" :options="lookups.consentProjects" v-model="user.constructionconsent_projects" multiple sm searchable/>
|
||||
</div>
|
||||
<div class="selected-items-viewer">
|
||||
<collapsible-selection-list title="Ausgewählte Netzgebiete" :items="user.preorder_networks" :lookup="lookups.networks" @remove="removeItem('preorder_networks', $event)"/>
|
||||
<collapsible-selection-list title="Ausgewählte Projekte" :items="user.constructionconsent_projects" :lookup="lookups.consentProjects" @remove="removeItem('constructionconsent_projects', $event)"/>
|
||||
</div>
|
||||
</tt-card>
|
||||
</transition>
|
||||
|
||||
<div class="user-edit-header collapsible" @click="toggleSection('security')">
|
||||
<h3>Passwort & API Key</h3>
|
||||
<i :class="getChevronClass('security')"></i>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<div v-show="!collapsedSections.security" class="user-form-grid-half">
|
||||
<tt-card>
|
||||
<div class="password-generation-grid">
|
||||
<tt-input label="Neues Passwort" v-model="password.new" :type="passwordFieldType" sm/>
|
||||
<tt-button icon="fas fa-sync-alt" @click="generatePassword" additional-class="btn-outline-secondary" sm title="Passwort generieren"/>
|
||||
<tt-button :icon="passwordFieldType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash'" @click="togglePasswordVisibility" additional-class="btn-outline-secondary" sm title="Passwort anzeigen"/>
|
||||
</div>
|
||||
<tt-input label="Passwort wiederholen" v-model="password.repeat" type="password" sm/>
|
||||
</tt-card>
|
||||
<tt-card>
|
||||
<label>API Key</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-sm" :value="user.apikey" readonly>
|
||||
<div class="input-group-append">
|
||||
<tt-button text="Neu generieren" @click="generateApiKey" additional-class="btn-outline-primary" sm confirm-text="Soll wirklich ein neuer API Key generiert werden? Der alte wird dadurch ungültig."/>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="user-edit-footer">
|
||||
<tt-tooltip :text="permissionChangesTooltip" position="top">
|
||||
<tt-button text="Speichern" icon="fas fa-save" additional-class="btn-primary" @click="saveUser" :loading="isSaving"/>
|
||||
</tt-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
`,
|
||||
components: {
|
||||
'collapsible-selection-list': {
|
||||
props: ['title', 'items', 'lookup', 'collapsible'],
|
||||
data: () => ({ collapsed: true }),
|
||||
computed: {
|
||||
selectedItems() {
|
||||
if (!this.items || !this.lookup) return [];
|
||||
const lookupMap = new Map(this.lookup.map(i => [i.value, i.text]));
|
||||
return this.items.map(id => ({ id, text: lookupMap.get(id) || `ID: ${id}` }));
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div v-if="items && items.length" class="selection-list-container">
|
||||
<strong @click="collapsed = !collapsed" style="cursor: pointer;">{{ title }} ({{ items.length }}) <i :class="collapsed ? 'fas fa-chevron-down' : 'fas fa-chevron-up'"></i></strong>
|
||||
<transition name="slide-fade-fast">
|
||||
<ul v-show="!collapsed" class="selection-list">
|
||||
<li v-for="item in selectedItems" :key="item.id">
|
||||
{{ item.text }}
|
||||
<i class="fas fa-times-circle" @click="$emit('remove', item.id)"></i>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: JSON.parse(JSON.stringify(window.TT_CONFIG.USER_DATA)), // Deep copy
|
||||
initialPermissions: {},
|
||||
lookups: window.TT_CONFIG.LOOKUPS,
|
||||
permissionsConfig: window.TT_CONFIG.PERMISSIONS_CONFIG,
|
||||
password: { new: '', repeat: '' },
|
||||
passwordFieldType: 'password',
|
||||
selectedTemplate: null,
|
||||
userToLoad: null,
|
||||
isSaving: false,
|
||||
isToggling: false,
|
||||
collapsedSections: {
|
||||
permissions: false,
|
||||
employeeSpecific: true,
|
||||
projects: true,
|
||||
security: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
templateOptions() {
|
||||
const options = this.lookups.permissionTemplates.map(t => ({ value: t.id, text: t.name }));
|
||||
options.unshift({ value: null, text: 'Vorlage auswählen...' });
|
||||
return options;
|
||||
},
|
||||
permissionChangesTooltip() {
|
||||
const added = [];
|
||||
const removed = [];
|
||||
for (const key in this.user.permissions) {
|
||||
const initial = !!this.initialPermissions[key];
|
||||
const current = !!this.user.permissions[key];
|
||||
if (initial !== current) {
|
||||
const permissionLabel = this.findPermissionLabel(key);
|
||||
if (!permissionLabel || /^\d+$/.test(permissionLabel)) continue;
|
||||
if (current) {
|
||||
added.push(`- ${permissionLabel}`);
|
||||
} else {
|
||||
removed.push(`- ${permissionLabel}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tooltipText = '';
|
||||
if (added.length > 0) {
|
||||
tooltipText += 'Hinzugefügt:\n' + added.join('\n');
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
if (tooltipText.length > 0) {
|
||||
tooltipText += '\n\n'; // Two line breaks
|
||||
}
|
||||
tooltipText += 'Entfernt:\n' + removed.join('\n');
|
||||
}
|
||||
|
||||
return tooltipText || 'Keine Berechtigungsänderungen';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSection(section) {
|
||||
this.collapsedSections[section] = !this.collapsedSections[section];
|
||||
},
|
||||
getChevronClass(section) {
|
||||
return this.collapsedSections[section] ? 'fas fa-chevron-down' : 'fas fa-chevron-up';
|
||||
},
|
||||
findPermissionLabel(key) {
|
||||
for (const group in this.permissionsConfig) {
|
||||
if (this.permissionsConfig[group][key]) {
|
||||
return this.permissionsConfig[group][key];
|
||||
}
|
||||
}
|
||||
return key;
|
||||
},
|
||||
applyTemplate(templateId) {
|
||||
if (!templateId) return;
|
||||
const template = this.lookups.permissionTemplates.find(t => t.id == templateId);
|
||||
template.permissions = JSON.parse(template.permissions);
|
||||
if (template) {
|
||||
const newPermissions = { ...this.user.permissions };
|
||||
for (const key in template.permissions) {
|
||||
newPermissions[key] = template.permissions[key];
|
||||
}
|
||||
this.user.permissions = newPermissions;
|
||||
window.notify('success', `Vorlage "${template.name}" angewendet.`);
|
||||
}
|
||||
this.selectedTemplate = null;
|
||||
},
|
||||
async loadDataFromUser(userId) {
|
||||
if(!userId) return;
|
||||
try {
|
||||
const response = await axios.get(`/UserEdit/getUserDataForTemplate?id=${userId}`);
|
||||
const dataToApply = response.data;
|
||||
|
||||
// Apply Permissions
|
||||
const newPermissions = { ...this.user.permissions };
|
||||
for (const key in newPermissions) {
|
||||
newPermissions[key] = dataToApply.permissions[key] === 'true';
|
||||
}
|
||||
this.user.permissions = newPermissions;
|
||||
|
||||
// Apply other fields
|
||||
this.$set(this.user, 'preorder_networks', dataToApply.preorder_networks || []);
|
||||
this.$set(this.user, 'constructionconsent_projects', dataToApply.constructionconsent_projects || []);
|
||||
this.$set(this.user, 'vodia_identity_domain', dataToApply.vodia_identity_domain || '');
|
||||
this.$set(this.user, 'vodia_identity_default', dataToApply.vodia_identity_default || '');
|
||||
|
||||
const selectedUser = this.lookups.users.find(u => u.value === userId);
|
||||
window.notify('success', `Daten von "${selectedUser.text}" geladen.`);
|
||||
} catch (error) {
|
||||
window.notify('error', 'Daten konnten nicht geladen werden.');
|
||||
} finally {
|
||||
this.userToLoad = null; // Reset autocomplete
|
||||
}
|
||||
},
|
||||
saveUser() {
|
||||
this.isSaving = true;
|
||||
if (this.password.new && this.password.new !== this.password.repeat) {
|
||||
window.notify('error', 'Die Passwörter stimmen nicht überein!');
|
||||
this.isSaving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
// Append standard fields
|
||||
const fields = ['id', 'username', 'name', 'email', 'mobile', 'address_id', 'employee_number', 'project_api_key', 'vodia_identity_domain', 'vodia_identity_username', 'vodia_identity_default'];
|
||||
fields.forEach(field => formData.append(field, this.user[field] || ''));
|
||||
|
||||
// Append booleans as 'true'/'false' strings
|
||||
formData.append('active', this.user.active ? 'true' : 'false');
|
||||
formData.append('twofactorrequired', this.user.twofactorrequired ? 'true' : 'false');
|
||||
|
||||
if (this.password.new) {
|
||||
formData.append('password', this.password.new);
|
||||
formData.append('password2', this.password.repeat);
|
||||
}
|
||||
|
||||
// Append ONLY TRUE permissions to mimic checkbox form behavior
|
||||
for (const key in this.user.permissions) {
|
||||
if (this.user.permissions[key]) {
|
||||
if (key.startsWith('can')) {
|
||||
formData.append(`can[${key.replace('can', '')}]`, 'true');
|
||||
} else {
|
||||
formData.append(key, 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(this.user.preorder_networks || []).forEach(val => formData.append('preorder_networks[]', val));
|
||||
(this.user.constructionconsent_projects || []).forEach(val => formData.append('constructionconsent_projects[]', val));
|
||||
|
||||
axios.post(window.TT_CONFIG.SAVE_URL, formData)
|
||||
.then(() => {
|
||||
window.notify('success', 'Benutzer erfolgreich gespeichert.');
|
||||
setTimeout(() => window.location.href = '/User', 150);
|
||||
})
|
||||
.catch(error => {
|
||||
window.notify('error', 'Fehler beim Speichern des Benutzers.');
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSaving = false;
|
||||
});
|
||||
},
|
||||
async generateApiKey() {
|
||||
try {
|
||||
await axios.post(window.TT_CONFIG.API_KEY_URL, new URLSearchParams({id: this.user.id}));
|
||||
window.notify('success', 'Neuer API Key wurde generiert. Seite wird neu geladen...');
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} catch (error) {
|
||||
window.notify('error', 'API Key konnte nicht generiert werden.');
|
||||
}
|
||||
},
|
||||
generatePassword() {
|
||||
const length = 14;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let retVal = "";
|
||||
for (let i = 0, n = charset.length; i < length; ++i) {
|
||||
retVal += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
this.password.new = retVal;
|
||||
this.password.repeat = retVal;
|
||||
window.notify('info', 'Neues Passwort generiert.');
|
||||
},
|
||||
togglePasswordVisibility() {
|
||||
this.passwordFieldType = this.passwordFieldType === 'password' ? 'text' : 'password';
|
||||
},
|
||||
removeItem(arrayName, valueToRemove) {
|
||||
const index = this.user[arrayName].indexOf(valueToRemove);
|
||||
if (index > -1) {
|
||||
this.user[arrayName].splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const permissions = {};
|
||||
Object.values(this.permissionsConfig).forEach(group => {
|
||||
Object.keys(group).forEach(key => {
|
||||
permissions[key] = this.user.permissions[key] === 'true' || this.user.permissions[key] === true;
|
||||
});
|
||||
});
|
||||
this.user.permissions = permissions;
|
||||
this.initialPermissions = JSON.parse(JSON.stringify(permissions)); // Deep copy for change tracking
|
||||
this.user.active = this.user.active == 1;
|
||||
this.user.twofactorrequired = this.user.twofactorrequired == 1;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
.template-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.template-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.template-list .list-group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.template-list .list-group-item .actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
/* Modal Styles */
|
||||
.permissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem 2rem;
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.3rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.permission-group h5 {
|
||||
font-size: 1.1rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
user-select: none;
|
||||
}
|
||||
147
public/js/pages/UserPermissionTemplate/UserPermissionTemplate.js
Normal file
147
public/js/pages/UserPermissionTemplate/UserPermissionTemplate.js
Normal file
@@ -0,0 +1,147 @@
|
||||
Vue.component('UserPermissionTemplate', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<div class="template-header">
|
||||
<h3>Berechtigungsvorlagen</h3>
|
||||
<tt-button text="Neue Vorlage erstellen" icon="fas fa-plus" additional-class="btn-primary" @click="openModal()"/>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
|
||||
|
||||
<ul v-else-if="templates.length" class="list-group list-group-flush template-list">
|
||||
<li v-for="template in templates" :key="template.id" class="list-group-item">
|
||||
<span>{{ template.name }}</span>
|
||||
<div class="actions">
|
||||
<tt-button icon="far fa-edit" additional-class="btn-outline-primary" sm @click="openModal(template)" title="Bearbeiten"/>
|
||||
<tt-button icon="fas fa-trash" additional-class="btn-outline-danger" sm @click="deleteTemplate(template)" title="Löschen"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="text-center text-muted p-5">
|
||||
Keine Vorlagen gefunden.
|
||||
</div>
|
||||
|
||||
<tt-modal v-if="showModal" :show.sync="showModal" :title="modalTitle" @submit="saveTemplate" :delete="false" :save-loading="isSaving" size="xl">
|
||||
<tt-input label="Vorlagename" v-model="editableTemplate.name" required sm/>
|
||||
<hr>
|
||||
<div class="permissions-grid">
|
||||
<div v-for="(group, groupName) in permissionsConfig" :key="groupName" class="permission-group">
|
||||
<h5>{{ groupName }}</h5>
|
||||
<div v-for="(label, key) in group" :key="key" class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" :id="'perm-' + key + modalKey" v-model="editableTemplate.permissions[key]">
|
||||
<label class="form-check-label" :for="'perm-' + key + modalKey" v-html="label"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tt-modal>
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
// Helper function to get a fresh permissions object. It's defined here to avoid relying on `this`.
|
||||
const getFreshPermissions = (config) => {
|
||||
const permissions = {};
|
||||
if (!config) return permissions;
|
||||
Object.values(config).forEach(group => {
|
||||
Object.keys(group).forEach(key => {
|
||||
permissions[key] = false;
|
||||
});
|
||||
});
|
||||
return permissions;
|
||||
};
|
||||
|
||||
return {
|
||||
loading: true,
|
||||
templates: [],
|
||||
showModal: false,
|
||||
isSaving: false,
|
||||
editableTemplate: {
|
||||
id: null,
|
||||
name: '',
|
||||
permissions: getFreshPermissions(window.TT_CONFIG.PERMISSIONS_CONFIG) // Initialize correctly
|
||||
},
|
||||
modalKey: Date.now(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
permissionsConfig() {
|
||||
return window.TT_CONFIG.PERMISSIONS_CONFIG;
|
||||
},
|
||||
modalTitle() {
|
||||
return this.editableTemplate && this.editableTemplate.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchTemplates() {
|
||||
this.loading = true;
|
||||
axios.get('/UserEdit/getPermissionTemplates')
|
||||
.then(response => {
|
||||
this.templates = response.data;
|
||||
})
|
||||
.catch(e => window.notify('error', 'Vorlagen konnten nicht geladen werden.'))
|
||||
.finally(() => this.loading = false);
|
||||
},
|
||||
openModal(template = null) {
|
||||
this.modalKey = Date.now(); // Ensure unique IDs for checkboxes in modal
|
||||
if (template) {
|
||||
this.editableTemplate = JSON.parse(JSON.stringify(template)); // Deep copy
|
||||
const permissions = this.freshPermissions();
|
||||
// Ensure all keys from config exist on the loaded template's permissions
|
||||
for (const key in permissions) {
|
||||
if (this.editableTemplate.permissions.hasOwnProperty(key)) {
|
||||
permissions[key] = this.editableTemplate.permissions[key];
|
||||
}
|
||||
}
|
||||
this.editableTemplate.permissions = permissions;
|
||||
} else {
|
||||
this.editableTemplate = { id: null, name: '', permissions: this.freshPermissions() };
|
||||
}
|
||||
this.showModal = true;
|
||||
},
|
||||
freshPermissions() {
|
||||
const permissions = {};
|
||||
Object.values(this.permissionsConfig).forEach(group => {
|
||||
Object.keys(group).forEach(key => {
|
||||
permissions[key] = false;
|
||||
});
|
||||
});
|
||||
return permissions;
|
||||
},
|
||||
async saveTemplate() {
|
||||
if (!this.editableTemplate.name) {
|
||||
return window.notify('error', 'Bitte einen Namen für die Vorlage eingeben.');
|
||||
}
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const response = await axios.post('/UserEdit/savePermissionTemplate', this.editableTemplate);
|
||||
if(response.data.success) {
|
||||
window.notify('success', response.data.message);
|
||||
this.showModal = false;
|
||||
this.fetchTemplates(); // Refresh list
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Fehler beim Speichern.');
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
},
|
||||
async deleteTemplate(template) {
|
||||
if (!confirm(`Soll die Vorlage "${template.name}" wirklich gelöscht werden?`)) return;
|
||||
try {
|
||||
const response = await axios.post('/UserEdit/deletePermissionTemplate', { id: template.id });
|
||||
if(response.data.success) {
|
||||
window.notify('success', response.data.message);
|
||||
this.fetchTemplates(); // Refresh list
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Fehler beim Löschen.');
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchTemplates();
|
||||
}
|
||||
});
|
||||
@@ -30,7 +30,7 @@ Vue.component('tt-tooltip', {
|
||||
<slot></slot> <div v-if="showTooltip" class="tt-tooltip-box"
|
||||
:style="{ whiteSpace: allowWrapping ? 'normal' : 'nowrap' }"
|
||||
:class="['tt-tooltip-' + position]">
|
||||
{{ text }}
|
||||
<div v-html="text.replaceAll('\\n', '<br>')"></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user