Merge branch 'master' into feature-36185/requestAnrechnung_bootstrap3_zu_bootstrap5

This commit is contained in:
SimonGschnell
2024-04-22 14:56:07 +02:00
31 changed files with 7674 additions and 117 deletions
+255
View File
@@ -0,0 +1,255 @@
<?php
if (!defined('BASEPATH')) exit('No direct script access allowed');
/**
* Controller using JSON
*/
class FHCAPI_Controller extends FHC_Controller
{
/**
* Response status
* @see https://github.com/omniti-labs/jsend
*/
const STATUS_SUCCESS = 'success';
const STATUS_FAIL = 'fail';
const STATUS_ERROR = 'error';
/**
* Error types
*/
const ERROR_TYPE_PHP = 'php'; // TODO(chris): php types from severity?
const ERROR_TYPE_EXCEPTION = 'exception';
const ERROR_TYPE_GENERAL = 'general';
const ERROR_TYPE_404 = '404';
const ERROR_TYPE_DB = 'db';
const ERROR_TYPE_VALIDATION = 'validation';
/**
* Return Object
*
* @var array
*/
private $returnObj = [];
/**
* Constructor
*
* @param array $requiredPermissions
* @return void
*/
public function __construct($requiredPermissions = [])
{
if (is_cli())
show_404();
parent::__construct();
$this->config->set_item('error_views_path', VIEWPATH.'errors'.DIRECTORY_SEPARATOR.'json'.DIRECTORY_SEPARATOR);
global $g_result;
$g_result = $this;
ob_start(function ($content) {
$http_response_code = http_response_code();
// NOTE(chris): For security reasons 404 will be displayed the same everywhere
if ($http_response_code == REST_Controller::HTTP_NOT_FOUND)
return $content;
header('Content-Type: application/json; charset=utf-8');
if (!isset($this->returnObj['meta']) || !isset($this->returnObj['meta']['status'])) {
switch ($http_response_code) {
case 200:
$this->setStatus(self::STATUS_SUCCESS);
break;
case 400:
$this->setStatus(self::STATUS_FAIL);
break;
default:
$this->setStatus(self::STATUS_ERROR);
break;
}
}
#$this->returnObj['test'] = implode('/n', headers_list());
return json_encode($this->returnObj);
});
// Load libraries
$this->load->library('AuthLib');
$this->load->library('PermissionLib');
// Checks if the caller is allowed to access to this content
$this->_isAllowed($requiredPermissions);
// For JSON Requests (as opposed to multipart/form-data) get the $_POST variable from the input stream instead
if ($this->input->get_request_header('Content-Type', true) == 'application/json')
$_POST = json_decode($this->security->xss_clean($this->input->raw_input_stream), true);
elseif (isset($_POST['_jsondata'])) {
$_POST = array_merge($_POST, json_decode($_POST['_jsondata'], true));
unset($_POST['_jsondata']);
}
}
// ---------------------------------------------------------------
// Handle Output object
// ---------------------------------------------------------------
/**
* @param array $data
* @param string $type (optional)
* @return void
*/
public function addError($data, $type = null)
{
if (!isset($this->returnObj['errors']))
$this->returnObj['errors'] = [];
$error = [];
if (is_array($data)) {
if ($type == self::ERROR_TYPE_VALIDATION)
$error['messages'] = $data;
else
$error = $data;
} else {
$error['message'] = $data;
}
if ($type)
$error['type'] = $type;
$this->returnObj['errors'][] = $error;
}
/**
* @param mixed $data
* @return void
*/
public function setData($data)
{
$this->returnObj['data'] = $data;
}
/**
* @param string $status
* @return void
*/
public function setStatus($status)
{
if (!isset($this->returnObj['meta']))
$this->returnObj['meta'] = [];
$this->returnObj['meta']['status'] = $status;
}
// ---------------------------------------------------------------
// Handle Output object - Shortcut functions
// ---------------------------------------------------------------
/**
* @param array $errors
* @return void
*/
protected function terminateWithValidationErrors($errors)
{
$this->output->set_status_header(REST_Controller::HTTP_BAD_REQUEST);
$this->addError($errors, self::ERROR_TYPE_VALIDATION);
$this->setStatus(self::STATUS_FAIL);
exit(EXIT_ERROR);
}
/**
* @param mixed $data (optional)
* @return void
*/
protected function terminateWithSuccess($data = null)
{
$this->setData($data);
$this->setStatus(self::STATUS_SUCCESS);
exit;
}
/**
* @param array $error
* @param string $type (optional)
* @return void
*/
protected function terminateWithError($error, $type = null)
{
$this->output->set_status_header(REST_Controller::HTTP_INTERNAL_SERVER_ERROR);
$this->addError($error, $type);
$this->setStatus(self::STATUS_ERROR);
exit;
}
/**
* @param stdclass $result
* @param string $errortype
* @return void
*/
protected function checkForErrors($result, $errortype = self::ERROR_TYPE_GENERAL)
{
// TODO(chris): IMPLEMENT!
if (isError($result)) {
$this->terminateWithError(getError($result), $errortype);
}
return $result->retval;
}
// TODO(chris): complete list
// ---------------------------------------------------------------
// Security
// ---------------------------------------------------------------
/**
* Checks if the caller is allowed to access to this content with the given permissions
* If it is not allowed will set the HTTP header with code 401
* Wrapper for permissionlib->isEntitled
*
* @param array $requiredPermissions
* @return void
*/
protected function _isAllowed($requiredPermissions)
{
// Checks if this user is entitled to access to this content
if (!$this->permissionlib->isEntitled($requiredPermissions, $this->router->method))
{
$this->output->set_status_header(isLogged() ? REST_Controller::HTTP_FORBIDDEN : REST_Controller::HTTP_UNAUTHORIZED);
$this->addError([
'message' => 'You are not allowed to access to this content',
'controller' => $this->router->class,
'method' => $this->router->method,
'required_permissions' => $this->_rpsToString($requiredPermissions, $this->router->method)
]);
exit; // immediately terminate the execution
}
}
/**
* Converts an array of permissions to a string that contains them as a comma separated list
* Ex: "<permission 1>, <permission 2>, <permission 3>"
*
* @param array $requiredPermissions
* @param string $method
* @return void
*/
protected function _rpsToString($requiredPermissions, $method)
{
if (!isset($requiredPermissions[$method]))
return '';
if (!is_array($requiredPermissions[$method]))
return $requiredPermissions[$method];
return implode(', ', $requiredPermissions[$method]);
}
}
@@ -0,0 +1,65 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
// NOTE(chris): For security reasons 404 will be displayed the same everywhere
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>404 Page Not Found</title>
<style type="text/css">
::selection { background-color: #E13300; color: white; }
::-moz-selection { background-color: #E13300; color: white; }
body {
background-color: #fff;
margin: 40px;
font: 13px/20px normal Helvetica, Arial, sans-serif;
color: #4F5155;
}
a {
color: #003399;
background-color: transparent;
font-weight: normal;
}
h1 {
color: #444;
background-color: transparent;
border-bottom: 1px solid #D0D0D0;
font-size: 19px;
font-weight: normal;
margin: 0 0 14px 0;
padding: 14px 15px 10px 15px;
}
code {
font-family: Consolas, Monaco, Courier New, Courier, monospace;
font-size: 12px;
background-color: #f9f9f9;
border: 1px solid #D0D0D0;
color: #002166;
display: block;
margin: 14px 0 14px 0;
padding: 12px 10px 12px 10px;
}
#container {
margin: 10px;
border: 1px solid #D0D0D0;
box-shadow: 0 0 8px #D0D0D0;
}
p {
margin: 12px 15px 12px 15px;
}
</style>
</head>
<body>
<div id="container">
<h1><?php echo $heading; ?></h1>
<?php echo $message; ?>
</div>
</body>
</html>
@@ -0,0 +1,49 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
global $g_result;
// NOTE(chris): remove p tags from CI_Exceptions::show_error() function
$msg = substr($message, 3);
$msg = substr($msg, 0, -4);
$msg = explode('</p><p>', $msg);
$msgs = [];
$error = [
'heading' => $heading
];
/** NOTE(chris): extract Error Number and SQL
* @see: DB_driver.php:692
*/
if (substr(current($msg), 0, 14) == 'Error Number: ') {
$code = substr(array_shift($msg), 14);
if ($code)
$error['code'] = (int)$code;
$msgs[] = array_shift($msg);
$error['sql'] = array_shift($msg);
}
/** NOTE(chris): extract Line Number and Filename
* @see: DB_driver.php:1782
* @see: DB_driver.php:1783
*/
if (count($msg) >= 2) {
if (substr(end($msg), 0, 13) == 'Line Number: ' && substr(prev($msg), 0, 10) == 'Filename: ') {
$error['line'] = (int)substr(array_pop($msg), 13);
$error['filename'] = substr(array_pop($msg), 10);
}
}
foreach ($msg as $m)
$msgs[] = $m;
if (count($msgs) == 1)
$error['message'] = current($msgs);
else
$error['messages'] = $msgs;
$g_result->addError($error, FHCAPI_Controller::ERROR_TYPE_DB);
$g_result->setStatus(FHCAPI_Controller::STATUS_ERROR);
@@ -0,0 +1,27 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
global $g_result;
$error = [
'message' => $message,
'class' => get_class($exception),
'filename' => $exception->getFile(),
'line' => $exception->getLine()
];
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === true) {
$error['backtrace'] = [];
foreach (debug_backtrace() as $err) {
if (isset($err['file']) && strpos($err['file'], realpath(BASEPATH)) !== 0) {
$error['backtrace'][] = [
'file' => $err['file'],
'line' => $err['line'],
'function' => $err['function']
];
}
}
}
$g_result->addError($error, FHCAPI_Controller::ERROR_TYPE_EXCEPTION);
$g_result->setStatus(FHCAPI_Controller::STATUS_ERROR);
@@ -0,0 +1,20 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
global $g_result;
// NOTE(chris): remove p tags from CI_Exceptions::show_error() function
$msg = substr($message, 3);
$msg = substr($msg, 0, -4);
$msg = explode('</p><p>', $msg);
$error = [
'heading' => $heading
];
if (count($msg) == 1)
$error['message'] = current($msg);
else
$error['messages'] = $msg;
$g_result->addError($error, FHCAPI_Controller::ERROR_TYPE_GENERAL);
$g_result->setStatus(FHCAPI_Controller::STATUS_ERROR);
@@ -0,0 +1,31 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
global $g_result;
$error = [
'message' => $message,
'severity' => $severity,
'filename' => $filepath,
'line' => $line
];
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === true) {
$error['backtrace'] = [];
foreach (debug_backtrace() as $err) {
if (isset($err['file']) && strpos($err['file'], realpath(BASEPATH)) !== 0) {
$error['backtrace'][] = [
'file' => $err['file'],
'line' => $err['line'],
'function' => $err['function']
];
}
}
}
// TODO(chris): change type with severity
$g_result->addError($error, 'php');
if (((E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity) {
$g_result->setStatus('error');
}
+81 -64
View File
@@ -1,83 +1,100 @@
.text-prewrap {
.fhc-header {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
margin-bottom: 3rem;
}
.fhc-header > h1:first-child {
font-size: calc(1.325rem + .9vw);
}
.fhc-header > :first-child > small {
color: var(--bs-secondary);
font-size: .65em;
padding-inline-start: 1em;
}
.fhc-alert.p-toast-center {
width: 35rem;
max-width: 100vw;
}
.fhc-alert.p-toast-top-right {
max-width: calc(100vw - 40px);
}
.fhc-alert.p-toast-top-right .p-toast-detail,
.fhc-alert.p-toast-center .p-toast-message-text .card {
white-space: pre-wrap;
}
.text-preline {
white-space: pre-line;
}
.accordion-button-primary {
background-color: #e7f1ff;
color: #0c63e4;
}
.accordion-button-primary:not(.collapsed) {
background-color: #cfe2ff;
color: #0a58ca;
.text-prewrap {
white-space: pre-wrap;
}
.accordion-button-secondary {
background-color: #f0f1f2;
color: #616971;
}
.accordion-button-secondary:not(.collapsed) {
background-color: #e2e3e5;
color: #565e64;
.btn-p-0 {
padding: 0 .375rem;
}
.accordion-button-success {
background-color: #e8f3ee;
color: #177a4c;
}
.accordion-button-success:not(.collapsed) {
background-color: #d1e7dd;
color: #146c43;
.z-1 {
z-index: 1;
}
.accordion-button-info {
background-color: #e7fafe;
color: #0cb6d8;
}
.accordion-button-info:not(.collapsed) {
background-color: #cff4fc;
color: #0aa2c0;
.input-group > .input-group-item {
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0;
}
.accordion-button-warning {
background-color: #fff9e6;
color: #e6ae06;
}
.accordion-button-warning:not(.collapsed) {
background-color: #fff3cd;
color: #cc9a06;
.input-group > .input-group-item .form-control:focus,
.input-group > .input-group-item .form-select:focus {
z-index: 3;
position: relative;
}
.accordion-button-danger {
background-color: #fcebec;
color: #c6303e;
}
.accordion-button-danger:not(.collapsed) {
background-color: #f8d7da;
color: #b02a37;
.input-group-lg > .input-group-item .form-control,
.input-group-lg > .input-group-item .form-select {
padding: 0.5rem 1rem;
font-size: 1.25rem;
border-radius: 0.3rem;
}
.accordion-button-light {
background-color: #fefeff;
color: #dfe0e1;
}
.accordion-button-light:not(.collapsed) {
background-color: #fefefe;
color: #c6c7c8;
.input-group-sm > .input-group-item .form-control,
.input-group-sm > .input-group-item .form-select {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: 0.2rem;
}
.accordion-button-dark {
background-color: #e9e9ea;
color: #1e2125;
}
.accordion-button-dark:not(.collapsed) {
background-color: #d3d3d4;
color: #1a1e21;
.input-group-lg > .input-group-item .form-select,
.input-group-sm > .input-group-item .form-select {
padding-right: 3rem;
}
.tabulator-edit-list .tabulator-edit-list-item {
background-color: white;
.input-group:not(.has-validation) > .input-group-item:not(:last-child) .form-control,
.input-group:not(.has-validation) > .input-group-item:not(:last-child) .form-select {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group.has-validation > .input-group-item:nth-last-child(n+3) .form-control,
.input-group.has-validation > .input-group-item:nth-last-child(n+3) .form-select {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > .input-group-item:not(:first-child) .form-control,
.input-group > .input-group-item:not(:first-child) .form-select {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.form-control-color.is-invalid,
.was-validated .form-control-color:invalid,
.form-control-color.is-valid,
.was-validated .form-control-color:valid {
padding-right: .375rem;
background-image: none;
}
.tabulator-edit-list .tabulator-edit-list-item:hover,
.tabulator-edit-list .tabulator-edit-list-item.active {
color: white;
}
@@ -64,3 +64,12 @@
margin-top: 20px;
}
.tabulator {
font-size: 1rem;
}
.tabulator-cell .btn {
padding: 0 .375rem;
font-size: .875rem;
border-radius: .2rem;
}
@@ -82,6 +82,7 @@
/*
* To be moved outside
*/
.navbar.navbar-left-side ~ *,
#content {
position: inherit;
margin: 0 0 0 250px;
+16
View File
@@ -0,0 +1,16 @@
.nav-item.nav-link:focus {
box-shadow: 0 0 0 .24rem rgba(13,110,253,.25);
z-index: 1;
outline: 0;
position: relative;
}
.nav-item.nav-link:focus::after {
content: "";
display: block;
position: absolute;
left: -.5rem;
right: -.5rem;
top: calc(100% + 1px);
background: white;
height: .25rem;
}
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
@import '../../../vendor/vuejs/vuedatepicker_css/main.css';
:root {
/*General*/
--dp-font-family: var(--bs-body-font-family);
--dp-border-radius: .25rem;
--dp-cell-border-radius: .25rem; /*Specific border radius for the calendar cell*/
/*Sizing*/
--dp-button-height: 2.1875rem; /*Size for buttons in overlays*/
--dp-month-year-row-height: 2.1875rem; /*Height of the month-year select row*/
--dp-month-year-row-button-size: 2.1875rem; /*Specific height for the next/previous buttons*/
--dp-button-icon-height: 1.25rem; /*Icon sizing in buttons*/
--dp-cell-size: 2.1875rem; /*Width and height of calendar cell*/
--dp-cell-padding: .3125rem; /*Padding in the cell*/
--dp-common-padding: .625rem; /*Common padding used*/
--dp-input-icon-padding: 2.1875rem;
--dp-input-padding: .375rem .75rem;
--dp-action-buttons-padding: .125rem .3125rem; /*Adjust padding for the action buttons in action row*/
--dp-row-margin: .3125rem 0; /*Adjust the spacing between rows in the calendar*/
--dp-calendar-header-cell-padding: 0.5rem; /*Adjust padding in calendar header cells*/
--dp-two-calendars-spacing: .625rem; /*Space between multiple calendars*/
--dp-overlay-col-padding: .1875rem; /*Padding in the overlay column*/
--dp-time-inc-dec-button-size: 2rem; /*Sizing for arrow buttons in the time picker*/
--dp-menu-padding: .375rem .5rem; /*Menu padding*/
}
.dp__theme_light {
--dp-text-color: var(--text-color);
--dp-primary-color: var(--bs-primary);
--dp-primary-disabled-color: rgba(var(--bs-primary-rgb), .65);
--dp-primary-text-color: var(--primary-color-text);
--dp-secondary-color: var(--bs-secondary);
--dp-border-color: var(--bs-gray-400);
--dp-menu-border-color: var(--bs-gray-400);
--dp-border-color-hover: var(--bs-gray-400);
--dp-icon-color: rgba(var(--bs-black-rgb), .5);
--dp-hover-icon-color: rgba(var(--bs-black-rgb), .75);
--dp-range-between-dates-text-color: var(--dp-hover-text-color, var(--text-color));
}
.dp__theme_light .form-control.is-invalid {
--dp-border-color-hover: var(--bs-danger);
}
.dp__theme_light .form-control.is-valid {
--dp-border-color-hover: var(--bs-success);
}
.form-control.is-invalid ~ .dp__clear_icon,
.was-validated .form-control:invalid ~ .dp__clear_icon { margin-right: calc(1.5em + .75rem - 12px) }
@@ -24,6 +24,8 @@ import {CoreRESTClient} from '../../RESTClient.js';
import {CoreFetchCmpt} from '../../components/Fetch.js';
import {BismeldestichtagAPIs} from './API.js';
import Phrasen from '../../plugin/Phrasen.js';
const bismeldestichtagApp = Vue.createApp({
data: function() {
return {
@@ -187,4 +189,4 @@ const bismeldestichtagApp = Vue.createApp({
}
});
bismeldestichtagApp.mount('#main');
bismeldestichtagApp.use(Phrasen).mount('#main');
+3 -1
View File
@@ -21,6 +21,8 @@ import {LogsViewerTabulatorEventHandlers} from './TabulatorSetup.js';
import {CoreFilterCmpt} from '../../components/filter/Filter.js';
import {CoreNavigationCmpt} from '../../components/navigation/Navigation.js';
import Phrasen from '../../plugin/Phrasen.js';
const logsViewerApp = Vue.createApp({
data: function() {
return {
@@ -40,5 +42,5 @@ const logsViewerApp = Vue.createApp({
}
});
logsViewerApp.mount('#main');
logsViewerApp.use(Phrasen).mount('#main');
+123
View File
@@ -0,0 +1,123 @@
import FhcFragment from "../Fragment.js";
export default {
components: {
FhcFragment
},
provide() {
return {
$registerToForm: component => {
if (this.inputs.indexOf(component) < 0)
this.inputs.push(component);
},
$clearValidationForName: this.clearValidationForName
};
},
props: {
tag: {
type: String,
default: 'form'
}
},
data() {
return {
inputs: []
}
},
computed: {
sortedInputs() {
return this.inputs.reduce((a,c) => {
let name = c.name || '_default';
if (!a[name])
a[name] = [];
a[name].push(c);
if (c.lcType == 'checkbox' && name.substr(-1) == ']' && name.indexOf('[')) {
name = name.substr(0, name.lastIndexOf('['));
if (!a[name])
a[name] = [];
a[name].push(c);
}
return a;
}, {});
},
factory() {
const factory = Object.create(Object.getPrototypeOf(this.$fhcApi.factory), Object.getOwnPropertyDescriptors(this.$fhcApi.factory));
factory.$fhcApi = {
get: this.get,
post: this.post
};
return factory;
}
},
methods: {
get(...args) {
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
args[0] = this;
else
args.unshift(this);
return this.$fhcApi.get(...args);
},
post(...args) {
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
args[0] = this;
else
args.unshift(this);
return this.$fhcApi.post(...args);
},
_sendFeedbackToInput(inputs, feedback, valid) {
if (inputs.length) {
inputs.forEach(input => input.setFeedback(valid, feedback));
return false;
}
if (this.$fhcAlert) {
this.$fhcAlert[valid ? 'alertSuccess' : 'alertError'](feedback);
return false;
}
return true;
},
setFeedback(valid, feedback) {
if (Array.isArray(feedback)) {
let remaining = feedback.filter(fb =>
this._sendFeedbackToInput(
this.sortedInputs['_default'] || [],
fb,
valid
)
);
return remaining.length ? remaining : null;
}
if (typeof feedback === 'object') {
let remaining = Object.entries(feedback).filter(([name, fb]) =>
this._sendFeedbackToInput(
this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [],
fb,
valid
)
);
return remaining.length ? Object.fromEntries(remaining) : null;
}
let remaining = this._sendFeedbackToInput(
this.sortedInputs['_default'] || [],
feedback,
valid
);
return remaining ? feedback : null;
},
clearValidation() {
this.inputs.forEach(input => input.clearValidation());
},
clearValidationForName(name) {
(this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [])
.forEach(input => input.clearValidation());
}
},
template: `
<component :is="tag || 'FhcFragment'" v-bind="$attrs">
<slot></slot>
</component>`
}
+315
View File
@@ -0,0 +1,315 @@
import FhcFragment from "../Fragment.js";
let _uuid = {};
export default {
inheritAttrs: false,
components: {
FhcFragment
},
inject: {
registerToForm: {
from: '$registerToForm',
default: null
},
clearValidationForName: {
from: '$clearValidationForName',
default: null
}
},
props: {
bsFeedback: Boolean,
noAutoClass: Boolean,
noFeedback: Boolean,
inputGroup: Boolean,
type: String,
name: String,
containerClass: [String, Array, Object]
},
data() {
return {
valid: undefined,
feedback: [],
modelValueDummy: undefined
}
},
computed: {
hasContainer() {
if (!this.bsFeedback)
return true;
if (this.containerClass)
return true;
for (const prop in this.autoContainerClass)
if (Object.hasOwn(this.autoContainerClass, prop))
return true;
return false;
},
acc() {
if (!this.containerClass)
return {};
if (typeof this.containerClass === 'string' || this.containerClass instanceof String)
return this.containerClass.split(' ').reduce((a,c) => {a[c] = true; return a}, {});
if (Array.isArray(this.containerClass))
return this.containerClass.reduce((a,c) => {a[c] = true; return a}, {});
return this.containerClass;
},
autoContainerClass() {
if (this.noAutoClass)
return this.acc;
const acc = {...this.acc};
if (this.inputGroup)
acc['input-group-item'] = true;
if (this.lcType == 'radio' || this.lcType == 'checkbox')
acc['form-check'] = true;
if (this.inputGroup && acc['form-check']) {
acc['input-group-item'] = false;
acc['form-check'] = false;
acc['input-group-text'] = true;
}
return acc;
},
lcType() {
if (!this.type)
return 'text';
return this.type.toLowerCase();
},
tag() {
switch (this.lcType) {
case 'textarea':
case 'select':
return this.lcType;
case 'datepicker':
return 'VueDatePicker';
case 'autocomplete':
return 'PvAutocomplete';
case 'uploadimage':
return 'UploadImage';
case 'uploadfile':
case 'uploaddms':
return 'UploadDms';
default:
return 'input';
}
},
validationClass() {
const classes = [];
if (this.valid)
classes.push('is-valid');
else if (this.valid === false)
classes.push('is-invalid');
if (!this.noAutoClass) {
let c = this.$attrs.class ? this.$attrs.class.split(' ') : [];
switch (this.lcType) {
// TODO(chris): complete list!
case 'select':
if (!c.includes('form-select'))
classes.push('form-select');
break;
case 'range':
if (!c.includes('form-range'))
classes.push('form-range');
break;
case 'radio':
case 'checkbox':
// TODO(chris): maybe different handling?
if (!c.includes('form-check-input') && !c.includes('btn-check'))
classes.push('form-check-input');
break;
case 'color':
if (!c.includes('form-control-color'))
classes.push('form-control-color');
if (!c.includes('form-control'))
classes.push('form-control');
break;
case 'autocomplete':
case 'datepicker':
classes.push('p-0');
classes.push('border-0');
if (!c.includes('form-control'))
classes.push('form-control');
break;
case 'text':
case 'number':
case 'password':
case 'textarea':
if (!c.includes('form-control'))
classes.push('form-control');
break;
}
}
return classes;
},
feedbackClass() {
if (!this.feedback || this.feedback === true)
return '';
if (!this.bsFeedback)
return {
'valid-tooltip': this.valid === true,
'invalid-tooltip': this.valid === false
};
return {
'valid-feedback': this.valid === true,
'invalid-feedback': this.valid === false
};
},
modelValueCmp: {
get() {
if (this.$attrs.modelValue === undefined)
return this.modelValueDummy;
return this.$attrs.modelValue;
},
set(v) {
if (this.$attrs.modelValue === undefined)
this.modelValueDummy = v;
this.$emit('update:modelValue', v);
}
},
idCmp() {
let uuid = this.$attrs.id;
if (this.lcType == 'datepicker')
uuid = this.$attrs.uid;
if (!uuid && this.$attrs.label)
uuid = 'fhc-form-input';
if (!uuid)
return undefined;
if (this.lcType == 'datepicker')
uuid = 'dp-input-' + uuid;
if (_uuid[uuid] === undefined)
_uuid[uuid] = 0;
return uuid + '-' + (_uuid[uuid]++);
}
},
methods: {
clearValidation() {
this.valid = undefined;
this.feedback = [];
},
clearValidationForThisName() {
if (this.valid === undefined)
return;
if (this.clearValidationForName && this.name)
this.clearValidationForName(this.name);
else
this.clearValidation();
},
setFeedback(valid, feedback) {
if (!feedback)
feedback = [];
if (!Array.isArray(feedback))
feedback = [feedback];
this.valid = valid;
// NOTE(chris): On a list of radios/checkboxes only add the feedback message to the last item
if (this.name && (this.lcType == 'radio' || this.lcType == 'checkbox')) {
const selector = 'input[type="' + this.lcType + '"][name="' + this.name + '"]';
if ([...this.$el.parentNode.querySelectorAll(selector)].pop() != this.$refs.input)
return;
}
this.feedback = feedback;
},
_loadComponents() {
if (this.tag == 'VueDatePicker' && !this._.components.VueDatePicker) {
this._.components.VueDatePicker = Vue.defineAsyncComponent(() => import("../vueDatepicker.js.php"));
} else if (this.tag == 'PvAutocomplete' && !this._.components.PvAutocomplete) {
this._.components.PvAutocomplete = Vue.defineAsyncComponent(() => import(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"));
} else if (this.tag == 'UploadImage' && !this._.components.UploadImage) {
this._.components.UploadImage = Vue.defineAsyncComponent(() => import("./Upload/Image.js"));
} else if (this.tag == 'UploadDms' && !this._.components.UploadDms) {
this._.components.UploadDms = Vue.defineAsyncComponent(() => import("./Upload/Dms.js"));
}
}
},
beforeMount() {
this._loadComponents();
},
beforeUpdate() {
this._loadComponents();
},
mounted() {
if (this.registerToForm)
this.registerToForm(this);
},
template: `
<component :is="!hasContainer ? 'FhcFragment' : 'div'" class="position-relative" :class="autoContainerClass">
<label v-if="$attrs.label && lcType != 'radio' && lcType != 'checkbox'" :for="idCmp">{{$attrs.label}}</label>
<input v-if="tag == 'input'" :type="lcType" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<textarea v-else-if="tag == 'textarea'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)"></textarea>
<select v-else-if="tag == 'select'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<slot></slot>
</select>
<component
v-else-if="tag == 'VueDatePicker'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:uid="idCmp ? idCmp.substr(9) : idCmp"
:name="name"
:class="validationClass"
:input-class-name=
"[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class-name'] ? $attrs['input-class-name'].split(' ') : [])].join(' ')"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else-if="tag == 'PvAutocomplete'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:input-props="{name}"
:class="validationClass"
:input-class="[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class'] ? $attrs['input-class'].split(' ') : [])].join(' ')"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else-if="tag == 'UploadDms'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:name="name"
:class="validationClass"
:input-class="validationClass"
:no-list="inputGroup"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:name="name"
:class="validationClass"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<label v-if="$attrs.label && (lcType == 'radio' || lcType == 'checkbox')" :for="idCmp" :class="!noAutoClass && 'form-check-label'">{{$attrs.label}}</label>
<div v-if="valid !== undefined && feedback.length && !noFeedback" :class="feedbackClass">
<template v-for="(msg, i) in feedback" :key="i">
<hr v-if="i" class="m-0">
{{msg}}
</template>
</div>
</component>
`
}
+87
View File
@@ -0,0 +1,87 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: {
type: [ FileList, Array ],
required: true
},
multiple: Boolean,
id: String,
name: String,
inputClass: [String, Array, Object],
noList: Boolean
},
methods: {
stringifyFile(file) {
return JSON.stringify({
lastModified: file.lastModified,
lastModifiedDate: file.lastModifiedDate,
name: file.name,
size: file.size,
type: file.type
});
},
addFiles(event) {
if (!this.multiple)
return this.$emit('update:modelValue', event.target.files);
const dt = new DataTransfer();
const doubles = [];
for (var file of this.modelValue) {
dt.items.add(file);
doubles.push(this.stringifyFile(file));
}
for (var file of event.target.files) {
// NOTE(chris): deep check (with FileReader) would require an async function so we only check the basic attributes
if (doubles.indexOf(this.stringifyFile(file)) < 0)
dt.items.add(file);
}
this.$emit('update:modelValue', dt.files);
},
removeFile(id) {
const fileToRemove = Array.from(this.modelValue)[id];
const dt = new DataTransfer();
for (var file of this.modelValue) {
if (file !== fileToRemove)
dt.items.add(file);
}
this.$emit('update:modelValue', dt.files);
}
},
watch: {
modelValue(n) {
if (n instanceof FileList)
return this.$refs.upload.files = n;
const dt = new DataTransfer();
const dms = [];
for (var file of n) {
if (file instanceof File) {
dt.items.add(file);
} else {
const dmsFile = new File([JSON.stringify(file)], file.name, {
type: 'application/x.fhc-dms+json'
});
dt.items.add(dmsFile);
}
}
this.$emit('update:modelValue', dt.files);
}
},
template: `
<div class="form-upload-dms">
<input ref="upload" class="form-control" :class="inputClass" :id="id" :name="name" :multiple="multiple" type="file" @change="addFiles">
<ul v-if="modelValue.length && multiple && !noList" class="list-unstyled m-0">
<li v-for="(file, index) in modelValue" :key="index" class="d-flex mx-1 mt-1 align-items-start">
<span class="col-auto"><i class="fa fa-file me-1"></i></span>
<span class="col">{{ file.name }}</span>
<button class="col-auto btn btn-outline-secondary btn-p-0" @click="removeFile(index)">
<i class="fa fa-close"></i>
</button>
</li>
</ul>
</div>`
}
+62
View File
@@ -0,0 +1,62 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: String
},
computed: {
valueAsBase64DataString() {
if (!this.modelValue || this.modelValue.substring(0, 10) == 'data:image')
return this.modelValue;
return 'data:image/jpeg;charset=utf-8;base64,' + this.modelValue;
}
},
methods: {
openUploadDialog() {
this.$refs.fileInput.click();
},
pickFile() {
let file = this.$refs.fileInput.files;
if (file && file[0]) {
let reader = new FileReader();
reader.onload = e => {
this.$emit('update:modelValue', e.target.result);
}
reader.readAsDataURL(file[0]);
}
},
deleteImage() {
this.$emit('update:modelValue', '');
}
},
template: `
<div class="form-upload-image">
<template v-if="modelValue">
<img class="img-thumbnail" :src="valueAsBase64DataString" />
<div class="fotobutton">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-outline-dark btn-sm" @click="deleteImage">
<i class="fa fa-close"></i>
</button>
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
<i class="fa fa-pen"></i>
</button>
</div>
</div>
</template>
<template v-else>
<slot>
<svg class="bd-placeholder-img img-thumbnail" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera: 200x200" preserveAspectRatio="xMidYMid slice" focusable="false"><title>A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera</title><rect width="100%" height="100%" fill="#868e96"></rect><text x="50%" y="50%" fill="#dee2e6" dy=".3em"></text></svg>
</slot>
<div class="fotobutton-visible">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
<i class="fa fa-pen"></i>
</button>
</div>
</div>
</template>
<input :id="$attrs.id" class="d-none" type="file" ref="fileInput" @input="pickFile" accept="image/*">
</div>`
}
+41
View File
@@ -0,0 +1,41 @@
export default {
inject: [
'$registerToForm'
],
data() {
return {
feedback: {
success: [],
danger: []
}
};
},
methods: {
clearValidation() {
this.feedback = {
success: [],
danger: []
};
},
setFeedback(valid, feedback) {
if (!feedback)
feedback = [];
if (!Array.isArray(feedback))
feedback = [feedback];
const ts = Date.now();
this.feedback[valid ? 'success' : 'danger'] = feedback.map(msg => [msg, ts]);
}
},
mounted() {
if (this.$registerToForm)
this.$registerToForm(this);
},
template: `
<template v-for="(arr, key) in feedback" :key="key">
<div v-for="[msg, ts] in arr" :key="ts + msg" class="alert alert-dismissible fade show" :class="'alert-' + key" role="alert">
{{msg}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</template>
`
};
+5
View File
@@ -0,0 +1,5 @@
export default {
render() {
return (this.$slots && this.$slots.default) ? this.$slots.default() : null;
}
};
+65 -24
View File
@@ -6,12 +6,19 @@ export default {
accessibility
},
emits: [
'update:modelValue'
'update:modelValue',
'change',
'changed'
],
props: {
configUrl: String,
config: {
type: [String, Object],
required: true
},
default: String,
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol]
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol],
vertical: Boolean,
border: Boolean
},
data() {
return {
@@ -35,50 +42,84 @@ export default {
}
}
},
created() {
CoreRESTClient
.get(this.configUrl)
.then(result => CoreRESTClient.getData(result.data))
.then(result => {
const tabs = {};
// TODO(chris): check if result is array
Object.entries(result).forEach(([key, config]) => {
if (!config.component)
watch: {
config(n) {
this.initConfig(n);
}
},
methods: {
change(key) {
this.$emit("change", key)
this.current = key;
this.$nextTick(() => this.$emit("changed", key));
},
initConfig(config) {
if (!config)
return;
if (typeof config === 'string' || config instanceof String)
return CoreRESTClient.get(config)
.then(result => CoreRESTClient.getData(result.data))
.then(this.initConfig)
.catch(this.$fhcAlert.handleSystemError);
const tabs = {};
if (Array.isArray(config)) {
config.forEach((item, key) => {
if (!item.component)
return console.error('Component missing for ' + key);
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(config.component))),
title: config.title || key,
config: config.config,
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
title: item.title || key,
config: item.config,
key
}
});
} else {
Object.entries(config).forEach(([key, item]) => {
if (!item.component)
return console.error('Component missing for ' + key);
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
title: item.title || key,
config: item.config,
key
}
});
}
if (this.current === null || !tabs[this.current]) {
if (tabs[this.default])
this.current = this.default;
else
this.current = Object.keys(tabs)[0];
this.tabs = tabs;
})
.catch(this.$fhcAlert.handleSystemError);
}
this.tabs = tabs;
}
},
created() {
this.initConfig(this.config);
},
template: `
<div class="fhc-tabs d-flex flex-column">
<div class="nav nav-tabs">
<div class="fhc-tabs d-flex" :class="vertical ? 'align-items-stretch gap-3' : (border ? 'flex-column' : 'flex-column gap-3')" v-if="Object.keys(tabs).length">
<div class="nav" :class="vertical ? 'nav-pills flex-column' : 'nav-tabs'">
<div
v-for="tab in tabs"
:key="tab.key"
class="nav-item nav-link"
:class="{active: tab.key == current}"
@click="current=tab.key"
@click="change(tab.key)"
:aria-current="tab.key == current ? 'page' : ''"
v-accessibility:tab
v-accessibility:tab.[vertical]
>
{{tab.title}}
</div>
</div>
<div style="flex: 1 1 0%; height: 0%" class="border-bottom border-start border-end overflow-auto p-3">
<div :style="vertical ? '' : 'flex: 1 1 0%; height: 0%'" class="overflow-auto" :class="vertical || !border ? '' : 'p-3 border-bottom border-start border-end'">
<keep-alive>
<component :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
<component ref="current" :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
</keep-alive>
</div>
</div>`
+18 -7
View File
@@ -136,9 +136,9 @@ export const CoreFilterCmpt = {
for (let col of columns)
{
// If the column has to be displayed or not
col.visible = selectedFields.indexOf(col.field) >= 0;
if (col.formatter == 'rowSelection')
col.visible = true;
/* fields.indexOf(col.field) == -1; ensures displaying formatter colums
e.g. column with rowSelection checkboxes or with custom formatted action buttons */
col.visible = selectedFields.indexOf(col.field) >= 0 || fields.indexOf(col.field) == -1;
if (col.hasOwnProperty('resizable'))
col.resizable = col.visible;
@@ -185,13 +185,23 @@ export const CoreFilterCmpt = {
else
this.getFilter();
},
initTabulator() {
async initTabulator() {
let placeholder = '< Phrasen Plugin not loaded! >';
if (this.$p) {
await this.$p.loadCategory('ui');
placeholder = this.$p.t('ui/keineDatenVorhanden');
}
// Define a default tabulator options in case it was not provided
let tabulatorOptions = {...{
height: 500,
layout: "fitColumns",
layout: "fitDataStretch",
movableColumns: true,
reactiveData: true
columnDefaults:{
tooltip: true,
},
placeholder,
reactiveData: true,
persistence: true
}, ...(this.tabulatorOptions || {})};
if (!this.tableOnly) {
@@ -265,6 +275,7 @@ export const CoreFilterCmpt = {
this.filterName = data.filterName;
this.dataset = data.dataset;
this.datasetMetadata = data.datasetMetadata;
this.fields = data.fields;
this.selectedFields = data.selectedFields;
this.notSelectedFields = this.fields.filter(x => this.selectedFields.indexOf(x) === -1);
@@ -435,7 +446,7 @@ export const CoreFilterCmpt = {
*
*/
handlerRemoveCustomFilter: function(event) {
filterId = event.currentTarget.getAttribute("href").substring(1);
let filterId = event.currentTarget.getAttribute("href").substring(1);
if (filterId === this.selectedFilter)
this.selectedFilter = null;
//
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (C) 2022 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
props: {
title: '',
subtitle: '',
mainCols: {
type: Array,
default: []
},
asideCols: {
type: Array,
default: []
},
},
computed: {
mainGridCols() {
return this.mainCols.length > 0 ? `col-md-${this.mainCols[0]}` : "col-md-12";
},
asideGridCols() {
return this.asideCols.length > 0 ? `col-md-${this.asideCols[0]}` : "";
}
},
template: `
<div class="overflow-hidden">
<header v-if="title">
<h1 class="h2 mb-5">{{ title }}<span class="fhc-subtitle">{{ subtitle }}</span></h1>
</header>
<div class="row gx-5">
<main :class="mainGridCols">
<slot name="main">{{ mainGridCols }}</slot>
</main>
<aside v-if="asideCols.length > 0" :class="asideGridCols">
<slot name="aside">{{ asideGridCols }}</slot>
</aside>
</div>
</div>
`
};
+11 -11
View File
@@ -133,8 +133,8 @@ const helperApp = Vue.createApp({
helperAppContainer.parentElement.removeChild(helperAppContainer);
},
template: `
<pv-toast ref="toast" base-z-index="99999"></pv-toast>
<pv-toast ref="alert" base-z-index="99999" position="center">
<pv-toast ref="toast" class="fhc-alert" :base-z-index="99999"></pv-toast>
<pv-toast ref="alert" class="fhc-alert" :base-z-index="99999" position="center">
<template #message="slotProps">
<i class="fa fa-circle-exclamation fa-2xl mt-3"></i>
<div class="p-toast-message-text">
@@ -161,7 +161,7 @@ const helperApp = Vue.createApp({
</a>
</div>
<div ref="messageCard" :id="'fhcAlertCollapseMessageCard' + slotProps.message.id" class="collapse mt-3">
<div class="card card-body text-body small" style="white-space: pre-wrap">
<div class="card card-body text-body small">
{{slotProps.message.detail}}
</div>
</div>
@@ -237,7 +237,7 @@ export default {
alertMultiple(messageArray, severity = 'info', title = 'Info', sticky = false){
// Check, if array has only string values
if (messageArray.every(message => typeof message === 'string')) {
messageArray.every(message => this.alertDefault(severity, title, message, sticky));
messageArray.forEach(message => this.alertDefault(severity, title, message, sticky));
return true;
}
return false;
@@ -281,21 +281,21 @@ export default {
handleSystemMessage(message) {
// Message is string
if (typeof message === 'string')
return fhcerror.alertWarning(message);
return $fhcAlert.alertWarning(message);
// Message is array of strings
if (Array.isArray(message)) {
// If Array has only Strings
if (message.every(msg => typeof msg === 'string'))
return message.every(fhcerror.alertWarning);
return message.every($fhcAlert.alertWarning);
// If Array has only Objects
if (message.every(msg => typeof msg === 'object') && msg !== null) {
return message.every(msg => {
if (msg.hasOwnProperty('data') && msg.data.hasOwnProperty('retval')) {
fhcerror.alertWarning(JSON.stringify(msg.data.retval));
$fhcAlert.alertWarning(JSON.stringify(msg.data.retval));
} else {
fhcerror.alertSystemError(JSON.stringify(msg));
$fhcAlert.alertSystemError(JSON.stringify(msg));
}
});
}
@@ -305,15 +305,15 @@ export default {
if (typeof message === 'object' && message !== null){
if (message.hasOwnProperty('data') && message.data.hasOwnProperty('retval')) {
// NOTE(chris): changed: alertSystemError => alertWarning
fhcerror.alertWarning(JSON.stringify(message.data.retval));
$fhcAlert.alertWarning(JSON.stringify(message.data.retval));
} else {
fhcerror.alertSystemError(JSON.stringify(message));
$fhcAlert.alertSystemError(JSON.stringify(message));
}
return;
}
// Fallback
fhcerror.alertSystemError('alertSystemError throws Generic Error\r\nError Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method);
$fhcAlert.alertSystemError('alertSystemError throws Generic Error\r\nError Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method);
},
resetFormValidation(form) {
const event = new Event('fhc-form-reset');
+282
View File
@@ -0,0 +1,282 @@
import FhcAlert from './FhcAlert.js';
import FhcApiFactory from '../apps/api/fhcapifactory.js';
export default {
install: (app, options) => {
app.use(FhcAlert);
function _get_config(form, uri, data, config) {
if (typeof form == 'string' && config === undefined) {
[uri, data, config] = [form, uri, data];
form = undefined;
} else if (form) {
if (typeof form != 'object')
throw new TypeError('Parameter 1 of _get_config must be an object or a string');
if (uri === undefined && data === undefined && config === undefined) {
config = form;
form = undefined;
}
}
if (form) {
// NOTE(chris): check if form is fhc-form
if (!form.clearValidation || !form.setFeedback)
throw new TypeError("'form' is not a Form Component");
form = {
clearValidation: form.clearValidation,
setFeedback: form.setFeedback
};
if (config)
config.form = form;
else
config = {form};
}
return [uri, data, config];
}
function _clean_return_value(response) {
const result = response.data;
delete response.data;
if (!result.meta)
result.meta = {response};
else
result.meta.response = response;
return result;
}
const fhcApiAxios = axios.create({
timeout: 5000,
baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/"
});
fhcApiAxios.interceptors.request.use(config => {
if (config.method != 'post' || !config.data)
return config;
if (config.data instanceof FormData)
return config;
if (!Object.values(config.data).every(item => {
if (item instanceof FileList)
return false;
if (Array.isArray(item))
return item.every(i => !(i instanceof File));
return true;
})) {
const newData = Object.entries(config.data).reduce((nd, [key, item]) => {
if (item instanceof FileList) {
for (const file of item)
nd.FormData.append(key + (item.length > 1 ? '[]' : ''), file);
} else if (Array.isArray(item)) {
if (item.every(i => !(i instanceof File))) {
nd.jsondata[key] = item;
} else {
item.forEach(file => nd.FormData.append(key + (item.length > 1 ? '[]' : ''), file));
}
} else {
nd.jsondata[key] = item;
}
return nd;
}, {
FormData: new FormData(),
jsondata: {}
});
newData.FormData.append('_jsondata', JSON.stringify(newData.jsondata));
config.data = newData.FormData;
}
return config;
});
fhcApiAxios.interceptors.response.use(response => {
if (response.config?.errorHandling == 'off'
|| response.config?.errorHandling === false
|| response.config?.errorHandling == 'fail')
return _clean_return_value(response);
// NOTE(chris): loop through errors
if (response.data.errors)
response.data.errors = response.data.errors.filter(
err => (response.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$fhcApi._defaultErrorHandlers[err.type])(err, response.config.form)
);
return _clean_return_value(response);
}, error => {
if (error.code == 'ERR_CANCELED')
return new Promise(() => {});
if (error.config?.errorHandling == 'off'
|| error.config?.errorHandling === false
|| error.config?.errorHandling == 'success')
return Promise.reject(error);
if (error.response) {
if (error.response.status == 404) {
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL, true);
return new Promise(() => {});
}
// NOTE(chris): loop through errors
error.response.data.errors = error.response.data.errors.filter(
err => (error.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$fhcApi._defaultErrorHandlers[err.type])(err, error.config.form)
);
if (!error.response.data.errors.length)
return new Promise(() => {});
} else if (error.request) {
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL);
return new Promise(() => {});
} else {
app.config.globalProperties.$fhcAlert.alertError(error.message);
return new Promise(() => {});
}
return Promise.reject(error);
});
app.config.globalProperties.$fhcApi = {
get(form, uri, params, config) {
[uri, params, config] = _get_config(form, uri, params, config);
if (params) {
if (config)
config.params = params;
else
config = {params};
}
return fhcApiAxios.get(uri, config);
},
post(form, uri, data, config) {
[uri, data, config] = _get_config(form, uri, data, config);
return fhcApiAxios.post(uri, data, config);
},
_defaultErrorHandlers: {
validation(error, form) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
if (form) {
form.clearValidation();
form.setFeedback(false, error.messages);
return false;
}
if (Array.isArray(error.messages)) {
error.messages.forEach($fhcAlert.alertError);
return false;
} else if (typeof error.messages == 'object') {
Object.entries(error.messages).forEach(
([key, value]) => $fhcAlert.alertDefault('error', key, value, true)
);
return false;
}
return true;
},
general(error, form) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
if (form)
form.setFeedback(false, error.message);
else
$fhcAlert.alertError(error.message);
},
php(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
switch (error.severity) {
case 'Warning':
case 'Core Warning':
case 'Compile Warning':
case 'User Warning':
$fhcAlert.alertDefault('warn', 'PHP ' + error.severity, message, true);
break;
case 'Notice':
case 'User Notice':
case 'Runtime Notice':
$fhcAlert.alertDefault('info', 'PHP ' + error.severity, message, true);
break;
default:
message = 'Type: PHP ' + error.severity + '\n\n' + message;
$fhcAlert.alertSystemError(message);
break;
}
},
exception(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Type: ' + error.class + '\n\n';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
$fhcAlert.alertSystemError(message);
},
db(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
if (error.heading !== undefined)
message += error.heading + '\n\n';
if (error.code !== undefined)
message += 'Code: ' + error.code + '\n\n';
if (error.sql !== undefined)
message += 'SQL: ' + error.sql + '\n\n';
if (error.message !== undefined)
message += 'Message: ' + error.message + '\n\n';
else if (error.messages !== undefined)
message += 'Messages: ' + error.messages.join('\n\t') + '\n\n';
if (error.filename !== undefined)
message += 'Filename: ' + error.filename + '\n';
if (error.line !== undefined)
message += 'Line Number: ' + error.line + '\n';
$fhcAlert.alertSystemError(message);
}
}
};
class FhcApiFactoryWrapper {
constructor(factorypart, root) {
if (root === undefined)
this.$fhcApi = app.config.globalProperties.$fhcApi;
else
Object.defineProperty(this, '$fhcApi', {
get() {
return (root || this).$fhcApi;
}
})
Object.keys(factorypart).forEach(key => {
Object.defineProperty(this, key, {
get() {
if (typeof factorypart[key] == 'function')
return factorypart[key].bind(this);
return new FhcApiFactoryWrapper(factorypart[key], root || this);
}
});
});
}
}
app.config.globalProperties.$fhcApi.factory = new FhcApiFactoryWrapper(FhcApiFactory);
}
};
+2
View File
@@ -51,6 +51,8 @@ require_once('dbupdate_3.4/29835_uhstat1_erfassung_der_uhstat1_daten_ueber_das_b
require_once('dbupdate_3.4/33714_erhoehter_studienbeitrag_fuer_drittsaatenangehoerig.php');
require_once('dbupdate_3.4/36275_zeitaufzeichnung_karenz.php');
require_once('dbupdate_3.4/21620_neues_feld_zum_erfassen_des_ESI.php');
require_once('dbupdate_3.4/36530_bis_internationsalisierung_codextabelle_neuerungen.php');
require_once('dbupdate_3.4/34543_ux_template.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
+16
View File
@@ -0,0 +1,16 @@
<?php
if (! defined('DB_NAME')) exit('No direct script access allowed');
// add app fhctemplate
if($result = $db->db_query("SELECT 1 FROM system.tbl_app WHERE app='fhctemplate'"))
{
if($db->db_num_rows($result) === 0)
{
$qry = "INSERT INTO system.tbl_app (app) VALUES('fhctemplate');";
if(!$db->db_query($qry))
echo '<strong>System Tabelle app: '.$db->db_last_error().'</strong><br>';
else
echo '<br>app fhctemplate hinzugefuegt';
}
}
@@ -0,0 +1,30 @@
<?php
if (! defined('DB_NAME')) exit('No direct script access allowed');
if($result = @$db->db_query("SELECT 1 FROM bis.tbl_mobilitaetsprogramm WHERE mobilitaetsprogramm_code IN (43, 44, 45, 46, 47, 48, 49, 50, 51, 52)"))
{
if($db->db_num_rows($result) < 10)
{
$qry = "
INSERT INTO bis.tbl_mobilitaetsprogramm(mobilitaetsprogramm_code, kurzbz, beschreibung, sichtbar, sichtbar_outgoing) VALUES
(43, 'AMinKunst', 'Auslandsstipendium des Bundesministeriums für Kunst', TRUE, TRUE),
(44, 'BundeslandProg', 'Bundesland-Programm', TRUE, TRUE),
(45, 'ERASMUSSMS', 'ERASMUS+ (SMS) - Studienaufenthalte', TRUE, TRUE),
(46, 'ERASMUSSMT', 'ERASMUS+ (SMT) - Studierendenpraktika', TRUE, TRUE),
(47, 'ERASMUSMundus', 'Erasmus Mundus Joint Master Degrees / Erasmus Mundus Joint Master', FALSE, FALSE),
(48, 'EUGrad', 'Mobilitätsprogramm für Graduierte im EU-Bereich', TRUE, TRUE),
(49, 'ÖStiftung', 'Stipendienstiftung der Republik Österreich', TRUE, FALSE),
(50, 'ÖAkadWiss', 'Stipendienabkommen der Österreichischen Akademie der Wissenschaften', TRUE, TRUE),
(51, 'FondWissForsch', 'Stipendium des Fonds zur Förderung der wissenschaftlichen Forschung', TRUE, TRUE),
(52, 'SEMP', 'Swiss-European Mobility Programme (SEMP)', TRUE, TRUE)
ON CONFLICT (mobilitaetsprogramm_code) DO NOTHING;
";
if(!$db->db_query($qry))
echo '<strong>bis.tbl_mobilitaetsprogramm: '.$db->db_last_error().'</strong><br>';
else
echo ' bis.tbl_mobilitaetsprogramm: Mobilitätsprogramme hinzugefuegt<br>';
}
}
+26
View File
@@ -1279,6 +1279,32 @@ $filters = array(
}
',
'oe_kurzbz' => null,
),
array(
'app' => 'fhctemplate',
'dataset_name' => 'exampledata',
'filter_kurzbz' => 'exampledata',
'description' => '{Beispieldaten Filter}',
'sort' => 1,
'default_filter' => true,
'filter' => '
{
"name": "Alle Beispieldaten",
"columns": [
{"name": "uid"},
{"name": "stringval"},
{"name": "integerval"},
{"name": "dateval"},
{"name": "booleanval"},
{"name": "moneyval"},
{"name": "dokument_bezeichnung"},
{"name": "textval"},
{"name": "examplestatus_kurzbz"}
],
"filters": []
}
',
'oe_kurzbz' => null
)
);
+240
View File
@@ -11395,6 +11395,26 @@ Any unusual occurrences
)
)
),
array(
'app' => 'core',
'category' => 'anrechnung',
'phrase' => 'anrechnung',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Anrechnung',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Exemption',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'anrechnung',
@@ -24196,7 +24216,227 @@ array(
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'global',
'phrase' => 'geloescht',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Gelöscht',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Deleted',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'global',
'phrase' => 'aenderungGespeichert',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Änderung gespeichert',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Saved changes',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'fhctemplate',
'category' => 'global',
'phrase' => 'datensatz',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Datensatz',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Dataset',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'fhctemplate',
'category' => 'global',
'phrase' => 'datensatzGenehmigen',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Datensatz genehmigen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Approve dataset',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'fhctemplate',
'category' => 'global',
'phrase' => 'datensatzAblehnen',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Datensatz ablehnen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Reject dataset',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'fhctemplate',
'category' => 'global',
'phrase' => 'datensatzAnlegen',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Datensatz anlegen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Add dataset',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'fhctemplate',
'category' => 'global',
'phrase' => 'datensatzBearbeiten',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Datensatz bearbeiten',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Edit dataset',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'global',
'phrase' => 'alleGenehmigt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Alle genehmigt',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'All accepted',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'global',
'phrase' => 'alleAbgelehnt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Alle abgelehnt',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'All rejected',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'global',
'phrase' => 'dokument',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Dokument',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Document',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'global',
'phrase' => 'aktionen',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Aktionen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Actions',
'description' => '',
'insertvon' => 'system'
)
)
)
);
+14 -9
View File
@@ -47,17 +47,23 @@ $tmp_gemeinde_ar = array();
if (isset($_FILES['parsefile']) && $_FILES['parsefile']['error'] == 0)
{
$rows = array_map('str_getcsv', file( $_FILES['parsefile']['tmp_name'] ));
$header = array_shift($rows);
$rows = file( $_FILES['parsefile']['tmp_name'] );
// all entries of csv
$header = explode(";",array_shift($rows)); // first row
$header = array_map('trim',$header);
$data = array();
foreach ($rows as $row)
{
$data[] = array_combine($header, $row);
$data[] = array_combine(preg_replace('/\xEF\xBB\xBF/','',$header), explode(";",$row));
}
foreach ($data as $gemeinde_details)
{
//Wenn nicht gültig dann überspringen
if ($gemeinde_details['Gültig'] == 'Nein') continue;
@@ -65,17 +71,16 @@ if (isset($_FILES['parsefile']) && $_FILES['parsefile']['error'] == 0)
$plzs = explode(' ', trim($gemeinde_details['PLZ']));
foreach ($plzs as $plz)
{
$tmp_obj_gemeinde = null;
{
$tmp_obj_gemeinde = new gemeinde();
$tmp_obj_gemeinde->plz = $plz;
$tmp_obj_gemeinde->kennziffer = $gemeinde_details["Gemeindekennziffer"];
$tmp_obj_gemeinde->name = $gemeinde_details['Gemeindename'];
$tmp_obj_gemeinde->ortschaftskennziffer = $gemeinde_details['Ortschaftskennziffer'];
$tmp_obj_gemeinde->ortschaftsname = $gemeinde_details['Ortschaftsname'];
$tmp_obj_gemeinde->bulacode = $gemeinde_details['BULA_Code'];
$tmp_obj_gemeinde->bulabez = $gemeinde_details['BULA_Bez'];
$tmp_obj_gemeinde->kennziffer = $gemeinde_details['Gemeindekennziffer'];
$tmp_obj_gemeinde->save();
$tmp_gemeinde_ar[] = $tmp_obj_gemeinde;
}