diff --git a/application/controllers/api/frontend/v1/Ampeln.php b/application/controllers/api/frontend/v1/Ampeln.php new file mode 100644 index 000000000..e0ffb4df5 --- /dev/null +++ b/application/controllers/api/frontend/v1/Ampeln.php @@ -0,0 +1,145 @@ +. + */ + +if (!defined('BASEPATH')) exit('No direct script access allowed'); + +class Ampeln extends FHCAPI_Controller +{ + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'open' => self::PERM_LOGGED, + 'all' => self::PERM_LOGGED, + 'confirm' => self::PERM_LOGGED, + 'alleAmpeln' => self::PERM_LOGGED, + ]); + + $this->load->model('content/Ampel_model', 'AmpelModel'); + $this->load->model('system/Sprache_model', 'SpracheModel'); + + $this->uid = getAuthUID(); + $this->pid = getAuthPersonID(); + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + /** + * confirms ampel and inserts ampel_id in public.tbl_ampel_benutzer_bestaetigt + * @access public + * + */ + public function confirm($ampel_id) + { + $this->load->library('form_validation'); + $this->form_validation->set_data(['ampel_id'=> $ampel_id]); + $this->form_validation->set_rules('ampel_id', 'Ampel ID', 'required|integer'); + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + // load Ampel_benutzer_bestaetigt_model to confirm the ampel + $this->load->model('content/Ampel_Benutzer_Bestaetigt_model', 'AmpelBenutzerBestaetigtModel'); + $insert_into_result = $this->AmpelBenutzerBestaetigtModel->insert(["ampel_id"=> $ampel_id, "uid"=> $this->uid]); + + $insert_into_result = $this->getDataOrTerminateWithError($insert_into_result); + + $this->terminateWithSuccess($insert_into_result); + } + + /** + * queries active and not confirmed ampeln by the user + * @access public + * + */ + public function open() + { + $userAmpeln = array(); + + // fetch active ampeln + $activeAmpeln = $this->AmpelModel->openActive($this->uid, false); + + $activeAmpeln = $this->getDataOrTerminateWithError($activeAmpeln); + + foreach ($activeAmpeln as $ampel) { + // only include non confirmed active ampeln in the result + if (!$ampel->bestaetigt) { + // check if the user was assigned to the ampel + $zugeteilt = $this->AmpelModel->isZugeteilt($this->uid, $ampel->benutzer_select); + + $zugeteilt = $this->getDataOrTerminateWithError($zugeteilt); + + if($zugeteilt) $userAmpeln[] = $ampel; + } + } + + $this->terminateWithSuccess($userAmpeln); + } + + /** + * queries all ampeln of the user + * @access public + * + */ + public function all() + { + $userAmpeln = array(); + + $ampel_result = $this->AmpelModel->active(false, $this->uid); + + $ampel_result = $this->getDataOrTerminateWithError($ampel_result); + + foreach ($ampel_result as $ampel) { + // check if the ampel was assigned to the user + $zugeteilt = $this->AmpelModel->isZugeteilt($this->uid, $ampel->benutzer_select); + + $zugeteilt = $this->getDataOrTerminateWithError($zugeteilt); + + if ($zugeteilt) $userAmpeln[] = $ampel; + } + + $this->terminateWithSuccess($userAmpeln); + } + + /** + * queries all ampeln that were assigned to the user until start of first work day + * @access public + * + */ + public function alleAmpeln() + { + + //fetch all ampeln + $alle_ampeln = $this->AmpelModel->alleAmpeln($this->uid); + + $alle_ampeln = $this->getDataOrTerminateWithError($alle_ampeln); + + $alle_ampeln = array_map(function ($ampel) { + // check if ampel is confirmed by user + $confirmedByUser = $this->AmpelModel->isConfirmed($ampel->ampel_id, $this->uid); + $ampel->bestaetigt = $confirmedByUser; + return $ampel; + }, $alle_ampeln); + + $this->terminateWithSuccess($alle_ampeln); + } +} + diff --git a/application/controllers/api/frontend/v1/Profil.php b/application/controllers/api/frontend/v1/Profil.php index ffb7449f8..63e86d1b2 100644 --- a/application/controllers/api/frontend/v1/Profil.php +++ b/application/controllers/api/frontend/v1/Profil.php @@ -18,11 +18,6 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); -/** - * This controller operates between (interface) the JS (GUI) and the SearchBarLib (back-end) - * Provides data to the ajax get calls about the searchbar component - * This controller works with JSON calls on the HTTP GET and the output is always JSON - */ class Profil extends FHCAPI_Controller { diff --git a/application/controllers/api/frontend/v1/ProfilUpdate.php b/application/controllers/api/frontend/v1/ProfilUpdate.php index 4b9dedb48..7a528b2de 100644 --- a/application/controllers/api/frontend/v1/ProfilUpdate.php +++ b/application/controllers/api/frontend/v1/ProfilUpdate.php @@ -18,11 +18,6 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); -/** - * This controller operates between (interface) the JS (GUI) and the SearchBarLib (back-end) - * Provides data to the ajax get calls about the searchbar component - * This controller works with JSON calls on the HTTP GET and the output is always JSON - */ class ProfilUpdate extends FHCAPI_Controller { diff --git a/application/helpers/hlp_return_object_helper.php b/application/helpers/hlp_return_object_helper.php index cc896856d..602008d3b 100755 --- a/application/helpers/hlp_return_object_helper.php +++ b/application/helpers/hlp_return_object_helper.php @@ -25,6 +25,7 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); /** * Used to create a return object, should not be used directly + * @return stdClass */ function _createReturnObject($code, $error, $retval) { @@ -39,7 +40,7 @@ function _createReturnObject($code, $error, $retval) /** * Success * - * @return array + * @return stdClass */ function success($retval = null, $code = null) { @@ -49,7 +50,7 @@ function success($retval = null, $code = null) /** * Error * - * @return array + * @return stdClass */ function error($retval = null, $code = null) { diff --git a/application/models/content/Ampel_Benutzer_Bestaetigt_model.php b/application/models/content/Ampel_Benutzer_Bestaetigt_model.php new file mode 100644 index 000000000..72373c2d8 --- /dev/null +++ b/application/models/content/Ampel_Benutzer_Bestaetigt_model.php @@ -0,0 +1,14 @@ +dbTable = 'public.tbl_ampel_benutzer_bestaetigt'; + $this->pk = 'ampel_benutzer_bestaetigt_id'; + } + +} diff --git a/application/models/content/Ampel_model.php b/application/models/content/Ampel_model.php index c50025a12..1f6cf9f93 100755 --- a/application/models/content/Ampel_model.php +++ b/application/models/content/Ampel_model.php @@ -16,37 +16,95 @@ class Ampel_model extends DB_Model * 1. not after the deadline date * 2. not before the vorlaufszeit * @param bool $email If true, then only ampeln are retrieved that are marked to be sent by mail. - * @return array Returns array of objects. + * @return stdClass Returns array of objects. */ - public function active($email = false) + public function active($email = false, $uid = null) { - $parametersArray = null; - $query = ' - SELECT * - FROM public.tbl_ampel - WHERE'; - - if ($email === true) - { - $parametersArray['email'] = $email; - $query .= ' email = ? AND'; + $userLanguage = getUserLanguage(); + $selectStatement='*,beschreibung[('.$this->getLanguageIndex($this->escape($userLanguage)).')] as beschreibung_trans, buttontext[('.$this->getLanguageIndex($this->escape($userLanguage)).')] as buttontext_trans'; + + if($uid != null ){ + $selectStatement .= ', + COALESCE(( + SELECT true + FROM public.tbl_ampel_benutzer_bestaetigt a + WHERE a.ampel_id = ' . $this->dbTable . '.ampel_id + AND uid = ' . $this->escape($uid) . ' LIMIT 1), false) as bestaetigt'; } - $query .= '( - (NOW()<(deadline+(COALESCE(verfallszeit,0) || \' days\')::interval)::date) - OR (verfallszeit IS NULL) - AND (NOW()>(deadline-(COALESCE(vorlaufzeit,0) || \' days\')::interval)::date) - OR (vorlaufzeit IS NULL AND NOW() < deadline))'; + $this->addSelect($selectStatement); + $whereStatement=''; - $query .= ' ORDER BY deadline DESC'; + if ($email === true) { + $whereStatement .= ' email = '.$this->escape($email).' AND'; + } + + $whereStatement .= + '( + ( + (NOW()<(deadline+(COALESCE(verfallszeit,0) || \' days\')::interval)::date) + OR (verfallszeit IS NULL) + ) + AND + ( + (NOW()>(deadline-(COALESCE(vorlaufzeit,0) || \' days\')::interval)::date) + OR (vorlaufzeit IS NULL AND NOW() < deadline) + ) + )'; + + $this->addOrder('deadline', 'DESC'); + return $this->loadWhere($whereStatement); + + } + + public function openActive($uid, $email = false) + { + $userLanguage = getUserLanguage(); + $selectStatement = '*,beschreibung[(' . $this->getLanguageIndex($this->escape($userLanguage)) . ')] as beschreibung_trans, buttontext[(' . $this->getLanguageIndex($this->escape($userLanguage)) . ')] as buttontext_trans'; + + + $selectStatement .= ', + COALESCE(( + SELECT true + FROM public.tbl_ampel_benutzer_bestaetigt a + WHERE a.ampel_id = ' . $this->dbTable . '.ampel_id + AND uid = ' . $this->escape($uid) . ' LIMIT 1), false) as bestaetigt'; + + $this->addSelect($selectStatement); + $whereStatement = ''; + + if ($email === true) { + $whereStatement .= ' email = ' . $this->escape($email) . ' AND'; + } + + $whereStatement .= + ' + (COALESCE(( + SELECT true + FROM public.tbl_ampel_benutzer_bestaetigt a + WHERE a.ampel_id = ' . $this->dbTable . '.ampel_id + AND uid = ' . $this->escape($uid) . ' LIMIT 1), false) = FALSE) AND + ( + ( + (NOW()<(deadline+(COALESCE(verfallszeit,0) || \' days\')::interval)::date) + OR (verfallszeit IS NULL) + ) + AND + ( + (NOW()>(deadline-(COALESCE(vorlaufzeit,0) || \' days\')::interval)::date) + OR (vorlaufzeit IS NULL AND NOW() < deadline) + ) + )'; + + $this->addOrder('deadline', 'DESC'); + return $this->loadWhere($whereStatement); - return $this->execQuery($query, $parametersArray); } /** * Returns all Ampel-receiver of a specific Ampel. * @param string $benutzer_select SQL Statement which defines the Ampel-receiver. - * @return array Returns array of objects with property 'uid'. + * @return stdClass Returns array of objects with property 'uid'. */ public function execBenutzerSelect($benutzer_select) { @@ -90,4 +148,101 @@ class Ampel_model extends DB_Model else return $result; //will contain the error-msg from execQuery } + + /** + * checks if a user is assigned to an ampel + * @param string $uid userID + * @param string $benutzer_select the select query which gets all the user that are assigned to an ampel + * @return stdClass + */ + public function isZugeteilt($uid, $benutzer_select){ + $zugeteilt = $this->execReadOnlyQuery(" + SELECT + CASE WHEN ? IN (".$benutzer_select.") + THEN true + ELSE false + END as zugeteilt + ", [$uid]); + + if(isError($zugeteilt)){ + return $zugeteilt; + } + + $zugeteilt = getData($zugeteilt); + + return success(current($zugeteilt)->zugeteilt); + } + + // THIS FUNCTION IS NOT IN USE + // fetches all ampeln that were assigned to the user after the working start_date + function alleAmpeln($uid){ + $userLanguage = getUserLanguage(); + + $zugeteile_ampeln = []; + + $datum = new datum(); + $now = $datum->mktime_fromdate(date('Y-m-d')); + + // start date of user + $benutzerStartDate = $this->execReadOnlyQuery(" + SELECT insertamum FROM public.tbl_benutzer WHERE uid = ?", [$uid]); + $benutzerStartDate = $datum->mktime_fromdate(date(current(getData($benutzerStartDate))->insertamum)); + + $allAmpeln = $this->execReadOnlyQuery(" + SELECT *, beschreibung[(".$this->getLanguageIndex($this->escape($userLanguage)).")] as beschreibung_trans, buttontext[(".$this->getLanguageIndex($this->escape($userLanguage)).")] as buttontext_trans FROM + public.tbl_ampel"); + + if(isError($allAmpeln)) return error(getError($allAmpeln)); + + $allAmpeln = getData($allAmpeln); + foreach($allAmpeln as $ampel){ + + // check if the ampel is assigned to the user + $zugeteilt = $this->execReadOnlyQuery(" + SELECT + CASE WHEN ? IN (".$ampel->benutzer_select.") + THEN true + ELSE false + END as zugeteilt + ", [$uid]); + + if(isError($zugeteilt)) return error(getError($zugeteilt)); + + $zugeteilt = current(getData($zugeteilt))->zugeteilt; + + + // abgelaufen check + // $now > strtotime('+' . $ampel->verfallszeit . ' day', $ampel->deadline) + + if( + // aktuelles datum liegt vor der Vorlaufzeit der Ampel + (isset($ampel->vorlaufzeit) && $now < strtotime('-' . $ampel->vorlaufzeit . ' day', $datum->mktime_fromdate($ampel->deadline))) + || + // ampel ist vor Arbeitsstart abgelaufen + (isset($ampel->verfallszeit) && $benutzerStartDate > strtotime('+' . $ampel->verfallszeit . ' day', $datum->mktime_fromdate($ampel->deadline))) + || + // ampel ist vor Arbeitsstart abgelaufen (verfallszeit nicht vorhanden) + ($benutzerStartDate > strtotime('+' . $ampel->verfallszeit . ' day', $datum->mktime_fromdate($ampel->deadline))) + ){ + // continue iteration if ampel is expired before work start or shouldn't be visible yet + continue; + } + + $ampel->zugeteilt = $zugeteilt; + + if($zugeteilt) $zugeteile_ampeln[] = $ampel; + + } + + return success($zugeteile_ampeln); + } + + private function getLanguageIndex($userLanguage) + { + return " + SELECT index + FROM public.tbl_sprache + WHERE sprache = " . $userLanguage; + } + } diff --git a/public/js/api/ampeln.js b/public/js/api/ampeln.js new file mode 100644 index 000000000..1c8ba81a2 --- /dev/null +++ b/public/js/api/ampeln.js @@ -0,0 +1,18 @@ +export default { + + open: function () { + return this.$fhcApi.get( + `/api/frontend/v1/Ampeln/open`,{}); + }, + + all: function () { + return this.$fhcApi.get( + `/api/frontend/v1/Ampeln/all`,{}); + }, + + confirm: function (ampel_id) { + return this.$fhcApi.get( + `/api/frontend/v1/Ampeln/confirm/${ampel_id}`,{}); + }, + +} \ No newline at end of file diff --git a/public/js/api/fhcapifactory.js b/public/js/api/fhcapifactory.js index 4aef3ff72..4627a7d5f 100644 --- a/public/js/api/fhcapifactory.js +++ b/public/js/api/fhcapifactory.js @@ -26,6 +26,7 @@ import bookmark from "./bookmark.js"; import stv from "./stv.js"; import notiz from "./notiz.js"; import betriebsmittel from "./betriebsmittel.js"; +import ampeln from "./ampeln.js"; export default { search, @@ -39,4 +40,5 @@ export default { stv, notiz, betriebsmittel + ampeln, }; diff --git a/public/js/apps/Dashboard/Admin.js b/public/js/apps/Dashboard/Admin.js index 426a2fce4..49d43657f 100755 --- a/public/js/apps/Dashboard/Admin.js +++ b/public/js/apps/Dashboard/Admin.js @@ -1,5 +1,6 @@ import {CoreNavigationCmpt} from '../../components/navigation/Navigation.js'; import DashboardAdmin from '../../components/Dashboard/Admin.js'; +import Phrasen from "../../plugin/Phrasen.js"; const app = Vue.createApp({ data: () => ({ @@ -11,4 +12,5 @@ const app = Vue.createApp({ } }); app.config.unwrapInjectedRef = true; +app.use(Phrasen); app.mount('#main'); diff --git a/public/js/components/DashboardWidget/Abstract.js b/public/js/components/DashboardWidget/Abstract.js index 25c5cfb9c..d459abc0b 100755 --- a/public/js/components/DashboardWidget/Abstract.js +++ b/public/js/components/DashboardWidget/Abstract.js @@ -29,12 +29,12 @@ export default { methods: { formatDateTime: function(dateTime) { const dt = new Date(dateTime); - return dt.getDate() + '.' + dt.getMonth() + '.' + dt.getFullYear() + ' | ' + return dt.getDate() + '.' + (dt.getMonth()+1) + '.' + dt.getFullYear() + ' | ' + dt.getHours() + ':' + dt.getMinutes(); }, getDate: function(dateTime){ const dt = new Date(dateTime); - return dt.getDate() + '.' + dt.getMonth() + '.' + dt.getFullYear(); + return dt.getDate() + '.' + (dt.getMonth()+1) + '.' + dt.getFullYear(); } } } diff --git a/public/js/components/DashboardWidget/Ampel.js b/public/js/components/DashboardWidget/Ampel.js index 9f0918aa7..be5db43ea 100755 --- a/public/js/components/DashboardWidget/Ampel.js +++ b/public/js/components/DashboardWidget/Ampel.js @@ -1,345 +1,273 @@ import AbstractWidget from './Abstract'; import BaseOffcanvas from '../Base/Offcanvas'; +let _idcounter = 0; + export default { - name: 'WidgetsAmpel', - components: { BaseOffcanvas }, - data: () => ({ - filter: '', - source: '', - ampeln: [] - }), - mixins: [ - AbstractWidget - ], - computed: { - widgetAmpeln () { - return this.ampeln.slice(0, 4); // show only newest 4 ampeln - }, - offcanvasAmpeln () - { - switch(this.filter) - { - case 'verpflichtend': return this.ampeln.filter(item => item.verpflichtend); - case 'ueberfaellig': return this.ampeln.filter(item => (new Date() > new Date(item.deadline)) && !item.bestaetigt); - default: return this.ampeln; - } - }, - count () { - return { - verpflichtend: this.ampeln.filter(item => item.verpflichtend).length, - ueberfaellig: this.ampeln.filter(item => (new Date() > new Date(item.deadline)) && !item.bestaetigt).length, - alle: this.ampeln.length - } - } - }, - methods: { - closeOffcanvasAmpeln() - { - for (let i = 0; i < this.offcanvasAmpeln.length; i++) - { - let ampelId = this.offcanvasAmpeln[i].ampel_id; - this.$refs['ampelCollapse_' + ampelId][0].classList.remove('show'); - } - }, - openOffcanvasAmpel(ampelId){ - // Close earlier opened Ampeln - this.closeOffcanvasAmpeln(); + name: 'WidgetsAmpel', + components: { BaseOffcanvas }, + data() { + return { + WIDGET_AMPEL_MAX: 4, + filter: '', + source: 'offen', + allAmpeln:null, + activeAmpeln:null, + idcounter: this.configMode ? 0 : ++_idcounter + }; + }, + mixins: [ + AbstractWidget + ], + computed: { + ampelnComputed() { + switch(this.source) { + case 'offen': return this.applyFilter(this.activeAmpeln); + case 'alle': return this.applyFilter(this.allAmpeln); + default: return this.applyFilter(this.activeAmpeln); + } + }, + ampelnOverview () { + return this.activeAmpeln?.slice(0, this.WIDGET_AMPEL_MAX); // show only newest 4 active ampeln + }, + count () { + const now = new Date(); + let datasource = this.activeAmpeln; + if (this.source == 'offen') datasource = this.activeAmpeln; + if (this.source == 'alle') datasource = this.allAmpeln; - // Show given Ampel - this.$refs['ampelCollapse_' + ampelId][0].classList.add('show'); - }, - closeOffcanvas(){ - this.filter = ''; - }, - confirm(ampelId){ - let indexToRemove = this.ampeln.findIndex((obj => obj.ampel_id === ampelId)); - this.ampeln.splice(indexToRemove, 1); - }, - changeDisplay(){ - this.filter = ''; - if (this.source == 'offen') - { - this.ampeln = TEST_OFFENE_AMPELN; - } + return { + verpflichtend: datasource?.filter(item => item.verpflichtend).length, + ueberfaellig: datasource?.filter(item => (now > new Date(item.deadline)) && !item.bestaetigt).length, + alle: datasource?.length + } + } + }, + methods: { + applyFilter(data) { + switch(this.filter) { + case 'verpflichtend': return data?.filter(item => item.verpflichtend); + case 'ueberfaellig': const now = new Date(); return data?.filter(item => (now > new Date(item.deadline)) && !item.bestaetigt); + default: return data; + } + }, + toggleFilter(value) { + this.filter === value ? this.filter = '' : this.filter = value; + }, + closeOffcanvasAmpeln() { + for (let i = 0; i < this.ampelnComputed.length; i++) + { + let ampelId = this.ampelnComputed[i].ampel_id; + if(this.$refs['ampelCollapse_' + ampelId]){ + this.$refs['ampelCollapse_' + ampelId][0].classList.remove('show'); + } + } + }, + openOffcanvasAmpel(ampelId) { + // Close earlier opened Ampeln + this.closeOffcanvasAmpeln(); - if (this.source == 'alle') - { - this.ampeln = TEST_ALLE_AMPELN; - // axios - // .get(this.apiurl + '/dashboard/Api/getAmpeln') - // .then(res => { this.ampeln = res.data }) - // .catch(err => { console.error('ERROR: ', err.response.data) }); - } - }, - validateBtnTxt(buttontext){ - return buttontext == null ? 'Bestätigen' : buttontext; - } - }, - created() { - this.$emit('setConfig', false); - this.ampeln = TEST_OFFENE_AMPELN; - }, - template: ` -
+ // Show given Ampel + this.$refs['ampelCollapse_' + ampelId][0].classList.add('show'); + }, + closeOffcanvas() { + this.closeOffcanvasAmpeln(); + this.filter = ''; + // maybe we also want to reset the source (open/alle) of the displayed ampeln + }, + fetchNonConfirmedActiveAmpeln() { + this.$fhcApi.factory + .ampeln.open() + .then(res => { + this.activeAmpeln = res.data; + }) + .catch(error => { + if (error.code === 'ECONNABORTED') + this.fetchNonConfirmedActiveAmpeln(); + else + this.$fhcAlert.handleSystemError(error); + }); + }, + fetchAllActiveAmpeln() { + this.$fhcApi.factory + .ampeln.all() + .then(res => { + this.allAmpeln = res.data; + }) + .catch(error => { + if (error.code === 'ECONNABORTED') + this.fetchAllActiveAmpeln(); + else + this.$fhcAlert.handleSystemError(error); + }); - -