User/rework
This commit is contained in:
@@ -74,6 +74,13 @@ $siteTitle = "Benutzer";
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="active">Aktiv:</label>
|
||||
<select name="active" id="active" class="form-control">
|
||||
<option value="false" <?=(isset($user) && !$user->active == 0) ? "selected='selected'" : ""?>>No</option>
|
||||
<option value="true" <?=(isset($user) && $user->active == 1) ? "selected='selected'" : ""?>>Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="technician">Techniker:</label>
|
||||
<select name="technician" id="technician" class="form-control">
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
$siteTitle = "Benutzer";
|
||||
|
||||
$pagination_baseurl = $this->getUrl($Mod, "Index");
|
||||
$pagination_baseurl_params = ["filter" => $filter];
|
||||
$pagination_entity_name = "Benutzer";
|
||||
?>
|
||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
|
||||
<link href="<?= self::getResourcePath() ?>assets/css/datatables-std.css?<?= date('U') ?>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
<style>
|
||||
.fa-exclamation-triangle, .fa-envelope, .fa-mobile-retro {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.fa-exclamation-triangle {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<!-- start page title -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="page-title-box">
|
||||
<div class="page-title-right">
|
||||
<ol class="breadcrumb m-0">
|
||||
<li class="breadcrumb-item"><a href="<?= self::getUrl("Dashboard") ?>"><?= MFAPPNAME_SLUG ?></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">Benutzer</li>
|
||||
</ol>
|
||||
</div>
|
||||
<h4 class="page-title">Benutzer</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end page title -->
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body mb-3">
|
||||
<div class="overflow-auto">
|
||||
<div class="float-left">
|
||||
<h4 class="header-title">Benutzerliste</h4>
|
||||
<p class="sub-header">
|
||||
<?php if (is_array($filter) && count($filter)): ?>
|
||||
<?php if ($filter['address_id']): ?>
|
||||
Zugehörig zu <?= (AddressModel::getOne($filter["address_id"])->getCompanyOrName()) ?>
|
||||
<br/>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<?php if (is_array($filter) && count($filter) && is_numeric($filter['address_id'])): ?>
|
||||
<a class="btn btn-primary"
|
||||
href="<?= self::getUrl("User", "add", ['address_id' => $filter['address_id']]) ?>"><i
|
||||
class="fas fa-plus"></i><span class="d-none d-lg-inline"> Neuen Benutzer anlegen</span></a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-primary" href="<?= self::getUrl("User", "add") ?>"><i
|
||||
class="fas fa-plus"></i><span class="d-none d-lg-inline"> Neuen Benutzer anlegen</span></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php //include(realpath(dirname(__FILE__)."/../")."/tpl/pagination.php"); ?>
|
||||
<?php //include(realpath(dirname(__FILE__)."/../")."/tpl/pagination-summary.php"); ?>
|
||||
<div style="clear: both;" class="mt-2">
|
||||
<table id="datatable" class="table table-striped table-hover font-13 table-sm ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Firma / Person</th>
|
||||
<th>Email</th>
|
||||
<th>Handy Nr.</th>
|
||||
<th>2FA</th>
|
||||
<th>Admin</th>
|
||||
<th>Techniker</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr id="filterrow">
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user):
|
||||
if ($user->twofactor == 0) {
|
||||
$twoFactortype = '<i class="fa fa-exclamation-triangle"><span style="display: none">N/A</span></i>';
|
||||
} else if ($user->twofactor == 1) {
|
||||
$twoFactortype = '<i class="fa-light fa-envelope"><span style="display: none">Mail</span></i>';
|
||||
} else if ($user->twofactor == 2) {
|
||||
$twoFactortype = '<i class="fa-light fa-mobile-retro"><span style="display: none">SMS</span></i>';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-nowrap"><?= $user->username ?></td>
|
||||
<td><?= $user->name ?></td>
|
||||
<td><?= ($user->address->company) ? $user->address->company : $user->address->getFullName() ?></td>
|
||||
<td><?= $user->email ?></td>
|
||||
<td><?= $user->mobile ?></td>
|
||||
<td class="text-center"><?= $twoFactortype ?></td>
|
||||
<td><?= ($user->isAdmin()) ? "Ja" : "Nein" ?></td>
|
||||
<td><?= ($user->is("Technician")) ? "Ja" : "Nein" ?></td>
|
||||
<td class="edit-width"
|
||||
style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
|
||||
<a href="<?= self::getUrl("User", "edit", ['id' => $user->id]) ?>"
|
||||
title="User bearbeiten"><i class="far fa-edit"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php //include(realpath(dirname(__FILE__)."/../")."/tpl/pagination-summary.php"); ?>
|
||||
<?php //include(realpath(dirname(__FILE__)."/../")."/tpl/pagination.php"); ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Control Sidebar -->
|
||||
<aside class="control-sidebar control-sidebar-dark">
|
||||
<!-- Control sidebar content goes here -->
|
||||
</aside>
|
||||
<!-- /.control-sidebar -->
|
||||
|
||||
<script type="text/javascript">
|
||||
var hidesearch = [8];
|
||||
var columndefs = {type: 'ip-address', targets: 4};
|
||||
|
||||
var columnfilter = [5];
|
||||
var columnoptions = '<option value=""></option><option value="SMS">SMS</option><option value="Mail">Mail</option><option value="N/A">N/A</option>';
|
||||
$(document).ready(function () {
|
||||
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript"
|
||||
src="<?= self::getResourcePath() ?>assets/js/datatables-std.js?<?= date('U') ?>"></script>
|
||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>
|
||||
@@ -38,12 +38,23 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="<?=self::getUrl("Dashboard","logout")?>" class="nav-link nav-user">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span class="pro-user-name ml-1">Abmelden</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- if $_SESSION[MFAPPNAME.'_impersonate'] is set then show a button that redirects to "User" "impersonate" "unimpersonate = true"-->
|
||||
<?php if(isset($_SESSION[MFAPPNAME.'_impersonate'])): ?>
|
||||
<li>
|
||||
<a href="<?=self::getUrl("User","impersonate",["unimpersonate"=>true])?>" class="nav-link nav-user">
|
||||
<i class="fas fa-user-secret text-white img-circle bg-info"></i>
|
||||
<span class="pro-user-name ml-1">Zurück als <?=$_SESSION[MFAPPNAME.'_username']?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li>
|
||||
<a href="<?=self::getUrl("Dashboard","logout")?>" class="nav-link nav-user">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span class="pro-user-name ml-1">Abmelden</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -24,26 +24,40 @@ class User extends mfBaseModel {
|
||||
/**
|
||||
* Loads currently logged in user
|
||||
*/
|
||||
public function loadMe() {
|
||||
if(defined("INTERNAL_USER_ID") && is_numeric(INTERNAL_USER_ID)) {
|
||||
$this->fetch(INTERNAL_USER_ID);
|
||||
return true;
|
||||
public function loadMe($ignoreImpersonate = false): bool
|
||||
{
|
||||
if (defined("INTERNAL_USER_ID") && is_numeric(INTERNAL_USER_ID)) {
|
||||
return $this->fetch(INTERNAL_USER_ID);
|
||||
}
|
||||
|
||||
$username = $_SESSION[MFAPPNAME.'_username'] ?? null;
|
||||
if (!$username) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = $this->db->select($this->table,"*","username='$username' and active=1 LIMIT 1");
|
||||
$user = null;
|
||||
if ($this->db->num_rows($res)) {
|
||||
$user = $this->db->fetch_object($res);
|
||||
// If Impersonating, load the user that is being impersonated
|
||||
if (isset($_SESSION[MFAPPNAME.'_impersonate']) && !$ignoreImpersonate) {
|
||||
$username = $_SESSION[MFAPPNAME.'_impersonate'];
|
||||
$res = $this->db->select($this->table, "*", "username='$username' and active=1 LIMIT 1");
|
||||
if ($this->db->num_rows($res)) {
|
||||
$user = $this->db->fetch_object($res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
mfLoginController::staticLogout();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load($user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!isset($_SESSION) || !is_array($_SESSION) || !array_key_exists(MFAPPNAME.'_username', $_SESSION)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$username = $_SESSION[MFAPPNAME.'_username'];
|
||||
$res = $this->db->select($this->table,"*","username='$username' LIMIT 1");
|
||||
if($this->db->num_rows($res)) {
|
||||
$data = $this->db->fetch_object($res);
|
||||
$this->load($data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function loadByUsername($username) {
|
||||
$username = $this->db->escape($username);
|
||||
if(!$username) {
|
||||
|
||||
@@ -13,7 +13,7 @@ class UserController extends mfBaseController
|
||||
{
|
||||
$this->needlogin = true;
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
$me->loadMe(true);
|
||||
$this->me = $me;
|
||||
$this->layout()->set("me", $me);
|
||||
|
||||
@@ -30,18 +30,25 @@ class UserController extends mfBaseController
|
||||
if (!$this->isAdmin()) {
|
||||
throw new Exception("Forbidden", 403);
|
||||
}
|
||||
$this->layout()->setTemplate('User/Index');
|
||||
|
||||
if ($this->request->filter) {
|
||||
$users = UserModel::search($this->request->filter);
|
||||
} else {
|
||||
$users = UserModel::getAll();
|
||||
}
|
||||
$this->layout()->set('users', $users);
|
||||
|
||||
//$addresses = AddressModel::getAll();
|
||||
//$this->layout()->set("addresses", $addresses);
|
||||
$this->layout()->set("filter", $this->request->filter);
|
||||
Helper::renderVue($this, "User", "Benutzer", [
|
||||
"IS_ADMIN" => $this->me->isAdmin(),
|
||||
"USERS" => array_map(fn($user) => [
|
||||
"username" => $user->username,
|
||||
"name" => $user->name,
|
||||
"address" => ($user->address->company) ? $user->address->company : $user->address->getFullName(),
|
||||
"email" => $user->email,
|
||||
"mobile" => $user->mobile,
|
||||
"twofactor" => [1 => 'Mail', 2 => 'SMS'][$user->twofactor] ?? 'N/A',
|
||||
"isAdmin" => $user->isAdmin(),
|
||||
"isTechnician" => $user->is("Technician"),
|
||||
"isActive" => $user->active,
|
||||
"id" => $user->id
|
||||
], UserModel::getAll()),
|
||||
"ADD_URL" => self::getUrl("User", "add"),
|
||||
"EDIT_URL" => self::getUrl("User", "edit"),
|
||||
"IMPERSONATE_URL" => self::getUrl("User", "impersonate"),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addAction($request)
|
||||
@@ -109,7 +116,7 @@ class UserController extends mfBaseController
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected function saveAction()
|
||||
{
|
||||
$r = $this->request;
|
||||
@@ -136,6 +143,8 @@ class UserController extends mfBaseController
|
||||
}
|
||||
}
|
||||
|
||||
$user->active = $r->active === "true" ? 1 : 0;
|
||||
|
||||
if (!$user->permissions) {
|
||||
$user->permissions = new WorkerPermission();
|
||||
}
|
||||
@@ -165,7 +174,7 @@ class UserController extends mfBaseController
|
||||
} else {
|
||||
$user->address_id = null;
|
||||
}
|
||||
|
||||
|
||||
// 2fa required
|
||||
if($r->twofactorrequired == "true") {
|
||||
$user->twofactorrequired = 1;
|
||||
@@ -201,7 +210,7 @@ class UserController extends mfBaseController
|
||||
} else {
|
||||
$user->permissions->employee = "false";
|
||||
}
|
||||
|
||||
|
||||
if ($r->technician == "true") {
|
||||
$user->permissions->technician = "true";
|
||||
} else {
|
||||
@@ -231,7 +240,7 @@ class UserController extends mfBaseController
|
||||
} else {
|
||||
$user->permissions->preorderreadonly = "false";
|
||||
}
|
||||
|
||||
|
||||
// set can permissions
|
||||
$user->permissions->canBuilding = "false";
|
||||
$user->permissions->canPipework = "false";
|
||||
@@ -253,7 +262,7 @@ class UserController extends mfBaseController
|
||||
$user->permissions->canWarehouseAdmin = "false";
|
||||
$user->permissions->canWarehouseEShop = "false";
|
||||
$user->permissions->canWarehouseUser = "false";
|
||||
|
||||
|
||||
if($r->get("can") && is_array($r->can)) {
|
||||
foreach($r->can as $key => $can) {
|
||||
//var_dump($key . "=> ".$can);
|
||||
@@ -262,22 +271,22 @@ class UserController extends mfBaseController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$user->permissions->save();
|
||||
|
||||
|
||||
// save networks
|
||||
$pn = $user->getFlag("preorder_networks");
|
||||
if (is_array($r->preorder_networks) && count($r->preorder_networks)) {
|
||||
$pn->value(json_encode($r->preorder_networks));
|
||||
$pn->save();
|
||||
|
||||
|
||||
$user->permissions->canPreorder = "true";
|
||||
$user->permissions->save();
|
||||
} else {
|
||||
$pn->delete();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// employee number
|
||||
$enum = new WorkerFlag($user->id, "employee_number");
|
||||
if($r->employee_number && $user->permissions->employee == "true") {
|
||||
@@ -371,14 +380,14 @@ class UserController extends mfBaseController
|
||||
|
||||
return $me->isAdmin();
|
||||
}
|
||||
|
||||
|
||||
protected function apiAction() {
|
||||
if(!$this->me->is(["Admin"])) {
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
$do = $this->request->do;
|
||||
$data = [];
|
||||
|
||||
|
||||
switch($do) {
|
||||
case "sse":
|
||||
$return = $this->startSuperexpertApi();
|
||||
@@ -401,25 +410,25 @@ class UserController extends mfBaseController
|
||||
$data['result'] = $return;
|
||||
$this->returnJson($data);
|
||||
}
|
||||
|
||||
|
||||
private function startSuperexpertApi() {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
|
||||
|
||||
if($me->superexpertEnabled() ) {
|
||||
// superexpert mode started already
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$me->superexpertStart(1800);
|
||||
|
||||
|
||||
return ["valid_to" => $me->getFlag("superexpert_lock_date")->value()];
|
||||
}
|
||||
|
||||
|
||||
private function extendSuperexpertApi() {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
|
||||
|
||||
if(!$me->superexpertEnabled() ) {
|
||||
// superexpert mode must be started already
|
||||
$this->log->debug("se not started");
|
||||
@@ -427,19 +436,19 @@ class UserController extends mfBaseController
|
||||
}
|
||||
$this->log->debug("ese");
|
||||
$me->superexpertExtend(1800);
|
||||
|
||||
|
||||
return ["valid_to" => $me->getFlag("superexpert_lock_date")->value()];
|
||||
}
|
||||
|
||||
|
||||
private function endSuperexpertApi() {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
|
||||
|
||||
if($me->superexpertEnabled() ) {
|
||||
$me->superexpertStop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return ["valid_to" => null];
|
||||
}
|
||||
|
||||
@@ -448,4 +457,24 @@ class UserController extends mfBaseController
|
||||
$user = new User($id);
|
||||
$this->returnJson($user->toArray());
|
||||
}
|
||||
|
||||
protected function impersonateAction() {
|
||||
if(!$this->me->isAdmin() || $this->me->address_id != 1) {
|
||||
header("HTTP/1.1 403 Forbidden");
|
||||
exit;
|
||||
}
|
||||
|
||||
if($this->request->unimpersonate) {
|
||||
unset($_SESSION[MFAPPNAME.'_impersonate']);
|
||||
$this->redirect("User");
|
||||
}
|
||||
|
||||
if(!$this->request->username || strlen($this->request->username) < 3) {
|
||||
header("HTTP/1.1 500 Internal Server Error");
|
||||
exit;
|
||||
}
|
||||
|
||||
$_SESSION[MFAPPNAME.'_impersonate'] = $this->request->username;
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class UserModel
|
||||
public $apikey = null;
|
||||
public $ip = null;
|
||||
public $sessionid = null;
|
||||
public $active = null;
|
||||
|
||||
|
||||
public $create_by = null;
|
||||
|
||||
25
db/migrations/20250314130000_woker_add_active.php
Normal file
25
db/migrations/20250314130000_woker_add_active.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class WorkerAddActive extends AbstractMigration {
|
||||
public function up(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
$workerTable = $this->table("Worker");
|
||||
$workerTable
|
||||
->addColumn("active", "integer", ['default' => 1])
|
||||
->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
$workerTable = $this->table("Worker");
|
||||
$workerTable
|
||||
->removeColumn("active")
|
||||
->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//SQL: ALTER TABLE Worker ADD COLUMN active INT DEFAULT 1;
|
||||
@@ -182,6 +182,7 @@ class mfLoginController extends mfBaseController
|
||||
return false;
|
||||
}
|
||||
$user = $this->db()->fetch_object($res);
|
||||
if ($user->active == 0) return false;
|
||||
$hash = $user->password;
|
||||
|
||||
#2FA Variablen
|
||||
|
||||
64
public/js/pages/User/User.js
Normal file
64
public/js/pages/User/User.js
Normal file
@@ -0,0 +1,64 @@
|
||||
Vue.component("User", {
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table :data="window['TT_CONFIG']['USERS']" :config="UserTableConfig">
|
||||
<template v-slot:top-buttons>
|
||||
<tt-button @click="window.location = window['TT_CONFIG']['ADD_URL']"
|
||||
additional-class="btn-primary"
|
||||
text="Vorbestellkampagne hinzuf\xfcgen"
|
||||
icon="fas fa-plus"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions="{ row: user }">
|
||||
<div class="d-flex justify-content-center" style="gap: 4px">
|
||||
<tt-button @click="window.location = window['TT_CONFIG']['EDIT_URL'] + '?id=' + user.id"
|
||||
additional-class="btn-outline-primary"
|
||||
sm
|
||||
icon="far fa-edit"
|
||||
title="Bearbeiten"/>
|
||||
|
||||
<tt-button @click="window.location = window['TT_CONFIG']['IMPERSONATE_URL'] + '?username=' + user.username"
|
||||
additional-class="btn-outline-secondary"
|
||||
sm
|
||||
title="Impersonate"
|
||||
icon="far fa-user-secret"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</tt-table>
|
||||
</tt-card>
|
||||
`, data: () => ({
|
||||
window: window, UserTableConfig: {
|
||||
key: "UserTable",
|
||||
tableHeader: "Benutzer",
|
||||
defaultPageSize: 25,
|
||||
headers: [{text: "Username", key: "username", class: "text-center", sortable: false, priority: 20},
|
||||
{text: "Name", key: "name", class: "text-center", sortable: false, priority: 18},
|
||||
{text: "Firma", key: "address", class: "text-center", priority: 19},
|
||||
{text: "E-Mail", key: "email", priority: 14},
|
||||
{text: "Tel. Nr.", key: "mobile", priority: 17, filter: false, sortable: false},
|
||||
{
|
||||
text: "2FA", key: "twofactor", class: "text-center", priority: 16, filter: 'iconSelect', sortable: false,
|
||||
filterOptions: [{value: "N/A", text: "N/A", icon: "fa fa-exclamation-triangle text-danger"},
|
||||
{value: "Mail", text: "Mail", icon: "fa-light fa-envelope text-primary"},
|
||||
{value: "SMS", text: "SMS", icon: "fa-light fa-mobile-retro text-info"}]
|
||||
},
|
||||
{
|
||||
text: "Admin", key: "isAdmin", class: "text-center", priority: 13, filter: 'iconSelect', sortable: false,
|
||||
filterOptions: [{value: true, text: "Ist Admin", icon: "fa-regular fa-circle-check text-success"},
|
||||
{value: false, text: "Ist kein Admin", icon: "fa-regular fa-circle-xmark text-danger"}]
|
||||
},
|
||||
{
|
||||
text: "Techniker", key: "isTechnician", class: "text-center", priority: 12, filter: 'iconSelect', sortable: false,
|
||||
filterOptions: [{value: true, text: "Ist Techniker", icon: "fa-regular fa-circle-check text-success"},
|
||||
{value: false, text: "Ist kein Techniker", icon: "fa-regular fa-circle-xmark text-danger"}],
|
||||
},
|
||||
{
|
||||
text: "Aktiv", key: "isActive", class: "text-center", priority: 12, filter: 'iconSelect', sortable: false,
|
||||
filterOptions: [{value: "1", text: "Ist Aktiv", icon: "fa-regular fa-circle-check text-success"},
|
||||
{value: "0", text: "Ist nicht aktiv", icon: "fa-regular fa-circle-xmark text-danger"}],
|
||||
},
|
||||
{text: "Aktionen", key: "actions", class: "text-center", sortable: false, priority: 21, filter: false}]
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -49,7 +49,7 @@ Vue.component('tt-icon-select', {
|
||||
<div class="form-group tt-select" style="user-select: none;margin-bottom: 0; margin-top: 6px">
|
||||
<div class="dropdown" :class="{'show': isOpen}">
|
||||
<i v-if="selectedOption !== null" :class="selectedOption.icon" style="font-size: 18px; cursor: pointer" ref="selectedIcon"></i>
|
||||
<span v-else style="cursor: pointer" ref="selectedIcon">Alle<i class="fas fa-caret-down"></i></span>
|
||||
<span v-else style="cursor: pointer" ref="selectedIcon">Alle<i class="ml-1 fas fa-caret-down"></i></span>
|
||||
<div style="display: grid; justify-items: center;" ref="select">
|
||||
<div class="dropdown-menu" :class="{'show': isOpen}" style="min-width: unset !important;">
|
||||
<a class="dropdown-item text-center" href="#" @click.prevent="selectOption(null)">Alle</a>
|
||||
|
||||
@@ -48,8 +48,12 @@ Vue.component('tt-table-pagination', {
|
||||
}
|
||||
return pages.length === 0 ? [1] : pages;
|
||||
}, pageInfoText() {
|
||||
const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1, this.pagination.total_rows);
|
||||
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total_rows);
|
||||
let start = Math.max(this.pagination.filtered_available, Math.min(
|
||||
this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1,
|
||||
this.pagination.total_rows
|
||||
));
|
||||
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.filtered_available);
|
||||
if (start > end) start = end;
|
||||
|
||||
if (!this.pagination.filtered_available) this.pagination.filtered_available = this.pagination.total_rows;
|
||||
|
||||
@@ -86,7 +90,7 @@ Vue.component('tt-table-pagination', {
|
||||
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
|
||||
</li>
|
||||
<li class="page-item tt-table-page-item"
|
||||
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
|
||||
v-bind:class="{ disabled: pagination.page === pagination.total_pages || pagination.total_pages <= 1 }">
|
||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
|
||||
aria-label="Last">
|
||||
<span aria-hidden="true">»</span>
|
||||
@@ -205,6 +209,7 @@ Vue.component('tt-table', {
|
||||
|
||||
|
||||
</span>
|
||||
<span v-else-if="column.filter === 'select'">{{ columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text }}</span>
|
||||
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</span>
|
||||
<span v-else-if="row[key] !== null"
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||
@@ -229,6 +234,7 @@ Vue.component('tt-table', {
|
||||
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
|
||||
.format('DD.MM.YYYY HH:mm')) : ''
|
||||
}}</span>
|
||||
<span v-else-if="column.filter === 'select'">{{ columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text }}</span>
|
||||
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
@@ -772,7 +778,8 @@ Vue.component('tt-table', {
|
||||
});
|
||||
|
||||
}
|
||||
// console.timeEnd('Filtering and pagination');
|
||||
// set page to current page or 1 if page is not set or 0 or less or over max pages
|
||||
if (this.pagination.page < 1) this.pagination.page = 1;
|
||||
return output;
|
||||
}, visibleRows() {
|
||||
if (!this.rawRows || this.ssr === true) return null;
|
||||
|
||||
77
scripts/constructionconsent/convert-xlsx.php
Normal file
77
scripts/constructionconsent/convert-xlsx.php
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test XLSX</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
#output { width: 100%; height: 400px; margin-top: 10px; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<input type="file" id="fileInput" accept=".xlsx">
|
||||
<script src="/plugins/xlsx/xlsx.min.js"></script>
|
||||
<script>
|
||||
document.getElementById('fileInput').addEventListener('change', handleFile);
|
||||
|
||||
function handleFile(e) {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
const data = new Uint8Array(e.target.result);
|
||||
const workbook = XLSX.read(data, {type: 'array', cellStyles: true});
|
||||
const jsonData = convertToJson(workbook);
|
||||
downloadJson(jsonData);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
function convertToJson(workbook) {
|
||||
const result = [];
|
||||
const headers = ["FLR", "KG", "GST", "EZ", "Vorname", "Nachname", "Straße", "PLZ, Ort"];
|
||||
|
||||
// Process first worksheet
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const range = XLSX.utils.decode_range(worksheet['!ref']);
|
||||
|
||||
for(let rowNum = 1; rowNum <= range.e.r; rowNum++) {
|
||||
const rowData = { fields: {}, bg_colors: {} };
|
||||
|
||||
headers.forEach((header, colIdx) => {
|
||||
const cellAddress = XLSX.utils.encode_cell({r: rowNum, c: colIdx});
|
||||
const cell = worksheet[cellAddress];
|
||||
|
||||
// Get cell value
|
||||
const value = cell ? cell.v : null;
|
||||
rowData.fields[header] = value;
|
||||
|
||||
// Get background color
|
||||
let hexColor = 'none';
|
||||
console.log(cell);
|
||||
if (cell?.s?.fgColor?.rgb) {
|
||||
hexColor = `#${cell.s.fgColor.rgb}`; // Remove alpha channel
|
||||
}
|
||||
rowData.bg_colors[header] = hexColor;
|
||||
});
|
||||
|
||||
result.push(rowData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function downloadJson(data) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'output.json';
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
195
scripts/constructionconsent/push-to-database.php
Normal file
195
scripts/constructionconsent/push-to-database.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
// Database configuration
|
||||
define('DB_HOST', 'test');
|
||||
define('DB_NAME', 'test');
|
||||
define('DB_USER', 'test');
|
||||
define('DB_PASS', 'test');
|
||||
|
||||
// Authentication
|
||||
$auth_code = 'test'; // Set this for basic security
|
||||
|
||||
// display errors
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
ini_set('error_reporting', E_ALL);
|
||||
|
||||
// Process form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['auth']) && $_POST['auth'] === $auth_code) {
|
||||
try {
|
||||
// Validate file upload
|
||||
if (!isset($_FILES['json_file']) || $_FILES['json_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('File upload error');
|
||||
}
|
||||
|
||||
// Verify JSON file
|
||||
$file_info = pathinfo($_FILES['json_file']['name']);
|
||||
if (strtolower($file_info['extension']) !== 'json') {
|
||||
throw new Exception('Invalid file type');
|
||||
}
|
||||
|
||||
// Read and decode JSON
|
||||
$json_data = file_get_contents($_FILES['json_file']['tmp_name']);
|
||||
$entries = json_decode($json_data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('Invalid JSON format');
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
$pdo = new PDO(
|
||||
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4",
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||
]
|
||||
);
|
||||
|
||||
$results = [
|
||||
'consents_created' => 0,
|
||||
'owners_created' => 0,
|
||||
'errors' => []
|
||||
];
|
||||
|
||||
// Process each entry
|
||||
foreach ($entries as $index => $entry) {
|
||||
try {
|
||||
// Normalize data
|
||||
$kg = (int)$entry['kg'];
|
||||
$gst = (string)$entry['gst'];
|
||||
$ez = (string)$entry['ez'];
|
||||
$street = substr($entry['street'], 0, 255);
|
||||
|
||||
// Find or create ConstructionConsent
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id FROM ConstructionConsent
|
||||
WHERE kg = ? AND gst = ? AND ez = ?
|
||||
");
|
||||
$stmt->execute([$kg, $gst, $ez]);
|
||||
$consent = $stmt->fetch();
|
||||
|
||||
if (!$consent) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO ConstructionConsent (
|
||||
constructionconsentproject_id, adb_strasse_id, object_type, name, ez, kg, gst,
|
||||
create_by, edit_by, `create`, `edit`
|
||||
) VALUES (
|
||||
1, ?, ?, ?, ?, ?, ?,
|
||||
145, 145, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
)
|
||||
");
|
||||
$stmt->execute([137947, "street", "GST: " . $gst, $ez, $kg, $gst]);
|
||||
$consent_id = $pdo->lastInsertId();
|
||||
$results['consents_created']++;
|
||||
} else {
|
||||
$consent_id = $consent['id'];
|
||||
}
|
||||
|
||||
// Prepare owner data
|
||||
$firstname = !isset($entry['firstname']) ? null : substr($entry['firstname'], 0, 255);
|
||||
$lastname = substr($entry['lastname'] ?? '', 0, 255);
|
||||
$street = substr($entry['street'], 0, 64);
|
||||
$zip = substr($entry['plz'], 0, 32);
|
||||
$city = substr($entry['ort'], 0, 64);
|
||||
|
||||
// Determine owner status and result
|
||||
$status = match(strtolower($entry['status'])) {
|
||||
'sent' => 'sent',
|
||||
'disallowed' => 'returned',
|
||||
'signed' => 'returned',
|
||||
default => 'new'
|
||||
};
|
||||
|
||||
$result = match(strtolower($entry['status'])) {
|
||||
'disallowed' => 'denied',
|
||||
'signed' => 'accepted',
|
||||
default => 'open'
|
||||
};
|
||||
|
||||
// Check existing owner
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id FROM ConstructionConsentOwner
|
||||
WHERE constructionconsent_id = ? AND (
|
||||
(firstname = ? AND lastname = ? AND street = ?) OR
|
||||
(firstname IS NULL AND lastname = ? AND street = ?))
|
||||
");
|
||||
$stmt->execute([$consent_id, $firstname, $lastname, $street, $lastname, $street]);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO ConstructionConsentOwner (
|
||||
constructionconsent_id, firstname, lastname, street,
|
||||
zip, city, status, result,
|
||||
create_by, edit_by, `create`, `edit`
|
||||
) VALUES (
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
145, 145, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
)
|
||||
");
|
||||
$stmt->execute([
|
||||
$consent_id, $firstname, $lastname, $street,
|
||||
$zip, $city, $status, $result
|
||||
]);
|
||||
$results['owners_created']++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$results['errors'][] = [
|
||||
'index' => $index,
|
||||
'message' => $e->getMessage(),
|
||||
'entry' => $entry
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Show results
|
||||
echo "<h3>Processing Results:</h3>";
|
||||
echo "<p>New consents created: " . $results['consents_created'] . "</p>";
|
||||
echo "<p>New owners created: " . $results['owners_created'] . "</p>";
|
||||
if (!empty($results['errors'])) {
|
||||
echo "<h4>Errors:</h4>";
|
||||
foreach ($results['errors'] as $error) {
|
||||
echo "<p style='color: red'>Error at index " . $error['index'] . ": " . $error['message'] . "</p>";
|
||||
echo "<pre>" . print_r($error['entry'], true) . "</pre>";
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "<p style='color: red'>Error: " . $e->getMessage() . "</p>";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Construction Consent Importer</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 2rem; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
form { border: 1px solid #ccc; padding: 2rem; border-radius: 5px; }
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
label { display: block; margin-bottom: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Upload Construction Consent JSON</h1>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label>Security Code:
|
||||
<input type="password" name="auth" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>JSON File:
|
||||
<input type="file" name="json_file" accept=".json" required>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">Process File</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php exit ?>
|
||||
117
scripts/constructionconsent/transform-json.php
Normal file
117
scripts/constructionconsent/transform-json.php
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JSON Processor</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
#output { width: 100%; height: 400px; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<input type="file" id="fileInput" accept=".json">
|
||||
<textarea id="output" readonly></textarea>
|
||||
|
||||
<script>
|
||||
document.getElementById('fileInput').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const inputData = JSON.parse(e.target.result);
|
||||
const transformed = transformData(inputData);
|
||||
document.getElementById('output').value = JSON.stringify(transformed, null, 2);
|
||||
} catch (error) {
|
||||
alert('Error parsing JSON: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
function hexToHSL(hex) {
|
||||
// Remove # if present
|
||||
let r = parseInt(hex.substring(1, 3), 16)/255;
|
||||
let g = parseInt(hex.substring(3, 5), 16)/255;
|
||||
let b = parseInt(hex.substring(5, 7), 16)/255;
|
||||
|
||||
let max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if(max === min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
let d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch(max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h *= 60;
|
||||
}
|
||||
|
||||
return { h: Math.round(h), s: Math.round(s*100), l: Math.round(l*100) };
|
||||
}
|
||||
|
||||
function getColorType(hue) {
|
||||
if((hue >= 0 && hue < 30) || hue >= 330) return 'red';
|
||||
if(hue >= 180 && hue < 270) return 'blue';
|
||||
if(hue >= 60 && hue < 180) return 'green';
|
||||
if(hue >= 45 && hue < 60) return 'yellow';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function transformData(data) {
|
||||
return data.map(item => {
|
||||
const [plz, ...ortParts] = item.fields["PLZ, Ort"]?.split(' ') || ['', ''];
|
||||
const ort = ortParts.join(' ');
|
||||
|
||||
const colorCounts = {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
yellow: 0,
|
||||
none: 0
|
||||
};
|
||||
|
||||
Object.values(item.bg_colors).forEach(color => {
|
||||
if(color === 'none') {
|
||||
colorCounts.none++;
|
||||
return;
|
||||
}
|
||||
|
||||
const { h } = hexToHSL(color);
|
||||
const colorType = getColorType(h);
|
||||
if(colorCounts[colorType] !== undefined) colorCounts[colorType]++;
|
||||
});
|
||||
|
||||
// Determine status
|
||||
const statusPriorities = ['red', 'green', 'yellow', 'blue'];
|
||||
const counts = statusPriorities.map(c => colorCounts[c]);
|
||||
const maxCount = Math.max(...counts);
|
||||
|
||||
let status = 'new';
|
||||
if(maxCount > 0) {
|
||||
if(colorCounts.red === maxCount) status = 'disallowed';
|
||||
else if(colorCounts.green === maxCount) status = 'signed';
|
||||
else if(colorCounts.yellow === maxCount) status = 'sent';
|
||||
}
|
||||
|
||||
return {
|
||||
kg: item.fields.KG,
|
||||
gst: item.fields.GST,
|
||||
ez: item.fields.EZ,
|
||||
firstname: item.fields.Vorname,
|
||||
lastname: item.fields.Nachname || '',
|
||||
street: item.fields.Straße || '',
|
||||
plz: plz,
|
||||
ort: ort,
|
||||
status: status
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user