diff --git a/application/controllers/Cis/Raumsuche.php b/application/controllers/Cis/Raumsuche.php new file mode 100644 index 000000000..055038275 --- /dev/null +++ b/application/controllers/Cis/Raumsuche.php @@ -0,0 +1,35 @@ + ['basis/cis:r'] + ]); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Public methods + + /** + * @return void + */ + public function index() + { + + $viewData = array( + 'uid'=>getAuthUID(), + ); + + $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Raumsuche']); + } +} diff --git a/application/controllers/api/frontend/v1/Ort.php b/application/controllers/api/frontend/v1/Ort.php index 8c4059824..828fbde2e 100644 --- a/application/controllers/api/frontend/v1/Ort.php +++ b/application/controllers/api/frontend/v1/Ort.php @@ -35,6 +35,8 @@ class Ort extends FHCAPI_Controller parent::__construct([ 'ContentID' => self::PERM_LOGGED, 'getOrtKurzbzContent' => self::PERM_LOGGED, + 'getRooms' => self::PERM_LOGGED, + 'getTypes' => self::PERM_LOGGED ]); $this->load->model('ressource/Ort_model', 'OrtModel'); @@ -44,6 +46,57 @@ class Ort extends FHCAPI_Controller //------------------------------------------------------------------------------------------------------------------ // Public methods + /** + * Retrieves all Ort entries filtered by the provided parameters + */ + public function getRooms() + { + // TODO: form validation + $datum = $this->input->get('datum', TRUE); + $von = $this->input->get('von', TRUE); + $bis = $this->input->get('bis', TRUE); + $typ = $this->input->get('typ', TRUE); + $personenanzahl = $this->input->get('personenanzahl', TRUE); + + $this->load->model('ressource/Stunde_model', 'StundeModel'); + $vonStunde = getData($this->StundeModel->getStundeForTime($von))[0]->stunde; + $bisStunde = getData($this->StundeModel->getStundeForTime($bis))[0]->stunde; + + $params = array(); + $qry = "SELECT DISTINCT tbl_ort.* + FROM public.tbl_ort JOIN public.tbl_ortraumtyp USING(ort_kurzbz) + WHERE aktiv AND lehre AND ort_kurzbz NOT LIKE '\\\\_%'"; + if($typ) { + $params[] = $typ; + $qry.= "AND raumtyp_kurzbz= ?"; + } + $qry.= "AND (max_person>= ? OR max_person is null)"; + $params[] = $personenanzahl; + + $qry.=" AND ort_kurzbz NOT IN + ( + SELECT ort_kurzbz FROM lehre.tbl_stundenplandev WHERE datum=? AND stunde>=? AND stunde<=? + UNION + SELECT ort_kurzbz FROM campus.tbl_reservierung WHERE datum=? AND stunde>=? AND stunde<=? + ) + "; + $params = array_merge($params, [$datum, $vonStunde, $bisStunde, $datum, $vonStunde, $bisStunde]); + + $result = $this->OrtModel->execReadOnlyQuery($qry, $params); + + $this->terminateWithSuccess($result); + } + + public function getTypes() + { + $this->load->model('ressource/Raumtyp_model', 'RaumtypModel'); + $result = $this->OrtModel->execReadOnlyQuery(" + SELECT * FROM public.tbl_raumtyp WHERE aktiv = true ORDER BY raumtyp_kurzbz; + "); + + $this->terminateWithSuccess(getData($result)); + } + /** * Gets a JSON body via HTTP POST and provides the parameters */ diff --git a/application/models/ressource/Stunde_model.php b/application/models/ressource/Stunde_model.php index 05a9cddff..0203163f7 100644 --- a/application/models/ressource/Stunde_model.php +++ b/application/models/ressource/Stunde_model.php @@ -11,4 +11,19 @@ class Stunde_model extends DB_Model $this->dbTable = 'lehre.tbl_stunde'; $this->pk = 'stunde'; } + + /** + * $time needs to be of PGSQL TIME format + */ + public function getStundeForTime($time) { + $query = " + SELECT min(stunde) as stunde FROM ( + SELECT stunde, extract(epoch from (beginn-?)) AS delta FROM lehre.tbl_stunde + UNION + SELECT stunde, extract(epoch from (ende-?)) AS delta FROM lehre.tbl_stunde + ) foo WHERE delta>=0 + "; + + return $this->execReadOnlyQuery($query, [$time, $time]); + } } diff --git a/application/views/CisRouterView/CisRouterView.php b/application/views/CisRouterView/CisRouterView.php index 1c81f3570..c03330c7a 100644 --- a/application/views/CisRouterView/CisRouterView.php +++ b/application/views/CisRouterView/CisRouterView.php @@ -5,7 +5,7 @@ $includesArray = array( 'axios027' => true, 'bootstrap5' => true, 'fontawesome6' => true, - 'tabulator5' => true, + 'tabulator5' => true, // TODO: upgrade to 6 when available 'vue3' => true, 'primevue3' => true, 'vuedatepicker11' => true, @@ -24,7 +24,8 @@ $includesArray = array( ), 'customJSs' => array( 'vendor/npm-asset/primevue/accordion/accordion.js', - 'vendor/npm-asset/primevue/accordiontab/accordiontab.js' + 'vendor/npm-asset/primevue/accordiontab/accordiontab.js', + 'vendor/npm-asset/primevue/inputnumber/inputnumber.js' ), 'customJSModules' => array( 'public/js/apps/Dashboard/Fhc.js' diff --git a/public/js/api/fhcapifactory.js b/public/js/api/fhcapifactory.js index 378d979ab..fe90a73fc 100644 --- a/public/js/api/fhcapifactory.js +++ b/public/js/api/fhcapifactory.js @@ -60,5 +60,5 @@ export default { addons, studiengang, menu, - authinfo, + authinfo }; diff --git a/public/js/api/ort.js b/public/js/api/ort.js index 4c8e2ce73..b2c1a78dd 100644 --- a/public/js/api/ort.js +++ b/public/js/api/ort.js @@ -7,4 +7,19 @@ export default { { ort_kurzbz: ort_kurbz } ); }, + getRooms(datum, von, bis, typ, personenanzahl = 0) { + return this.$fhcApi.get( + FHC_JS_DATA_STORAGE_OBJECT.app_root + + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + "/api/frontend/v1/Ort/getRooms", + { datum, von, bis, typ, personenanzahl } + ); + }, + getRoomTypes() { + return this.$fhcApi.get( + FHC_JS_DATA_STORAGE_OBJECT.app_root + + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + "/api/frontend/v1/Ort/getTypes" + ); + } } \ No newline at end of file diff --git a/public/js/apps/Dashboard/Fhc.js b/public/js/apps/Dashboard/Fhc.js index b8ec122e0..6a46c5999 100644 --- a/public/js/apps/Dashboard/Fhc.js +++ b/public/js/apps/Dashboard/Fhc.js @@ -6,6 +6,7 @@ import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; import Stundenplan, {DEFAULT_MODE_STUNDENPLAN} from "../../components/Cis/Stundenplan/Stundenplan.js"; import MylvStudent from "../../components/Cis/Mylv/Student.js"; import Profil from "../../components/Cis/Profil/Profil.js"; +import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js"; import CmsNews from "../../components/Cis/Cms/News.js"; import CmsContent from "../../components/Cis/Cms/Content.js"; import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js"; @@ -28,7 +29,12 @@ const router = VueRouter.createRouter({ component: Profil, props: true }, - + { + path: `/Cis/Raumsuche`, + name: 'Raumsuche', + component: Raumsuche, + props: true + }, // Redirect old links to new format { path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz", diff --git a/public/js/components/Cis/Mylv/Student.js b/public/js/components/Cis/Mylv/Student.js index 5aab9b28a..300ed44f8 100644 --- a/public/js/components/Cis/Mylv/Student.js +++ b/public/js/components/Cis/Mylv/Student.js @@ -76,6 +76,7 @@ export default { this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true })); }, setHash(val) { + // TODO: make this a router param to enable history location.hash = val; } }, diff --git a/public/js/components/Cis/Raumsuche/Raumsuche.js b/public/js/components/Cis/Raumsuche/Raumsuche.js new file mode 100644 index 000000000..d7660ef11 --- /dev/null +++ b/public/js/components/Cis/Raumsuche/Raumsuche.js @@ -0,0 +1,237 @@ + +import {CoreFilterCmpt} from "../../../components/filter/Filter.js"; + +export default { + name: "Raumsuche", + props: { + + }, + components: { + VueDatePicker, + CoreFilterCmpt, + InputNumber: primevue.inputnumber, + }, + data() { + return { + tabulatorUuid: Vue.ref(0), + tableBuiltResolve: null, + tableBuiltPromise: null, + roomtypes: null, + defaultType: { + raumtyp_kurzbz: '', + beschreibung: Vue.computed(() => this.$p.t('global/alle')) + }, + anzahl: 1, + selectedType: null, + datum: new Date(), + von: Vue.ref({ + hours: new Date().getHours(), + minutes: new Date().getMinutes() + }), + bis: Vue.ref({ + hours: new Date().getHours() + 1, + minutes: new Date().getMinutes() + }), + raumsucheTableOptions: { + height: Vue.ref(400), + index: 'prestudent_id', + layout: 'fitColumns', + placeholder: this.$p.t('global/noDataAvailable'), + columns: [ + {title: Vue.computed(() => this.$p.t('rauminfo/raum_kurzbz')), field: 'ort_kurzbz', widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('global/bezeichnung')), field: 'bezeichnung', widthGrow: 2}, + {title: Vue.computed(() => this.$p.t('global/nummer')), field: 'nummer', widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('global/personen')), field: 'personen', widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('rauminfo/raumInfo')), + field: 'linkInfo', formatter: this.linkFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$p.t('rauminfo/roomReservations')), + field: 'linkRes', formatter: this.linkFormatter, widthGrow: 1} + ], + persistence: false, + }, + raumsucheTableEventHandlers: [{ + event: "tableBuilt", + handler: async () => { + this.tableBuiltResolve() + } + }, + { + event: "cellClick", + handler: async (e, cell) => { + + if((cell.column.field === 'linkInfo' || cell.column.field === 'linkRes') && cell.value){ + window.open(cell.value, '_blank'); + e.stopPropagation(); + } + + } + } + ]}; + }, + methods: { + tableResolve(resolve) { + this.tableBuiltResolve = resolve + }, + linkFormatter(cell) { + const val = cell.getValue() + if(val) { + return '
' + + '
' + } else { + return '
' + + '-
' + } + }, + roomPlanLink(room) { + return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + '/CisVue/Cms/getRoomInformation/' + room.ort_kurzbz + }, + roomInfoLink(room) { + return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + '/CisVue/Cms/content/' + room.content_id + }, + getTimeString(time) { + return `${time.hours}:${time.minutes}:00` + }, + setupData(data){ + const d = data.map(room => { + return { + ort_kurzbz: room.ort_kurzbz, + bezeichnung: room.bezeichnung, + nummer: room.planbezeichnung, + personen: room.max_person, + linkInfo: room.content_id ? this.roomInfoLink(room) : null, + linkRes: this.roomPlanLink(room) + + } + + }) + + this.$refs.raumsucheTable.tabulator.setData(d); + }, + loadRoomTypes() { + this.$fhcApi.factory.ort.getRoomTypes().then(res => { + this.selectedType = this.defaultType + this.roomtypes = res?.data ?? [] + }) + }, + loadRooms() { + this.$fhcApi.factory.ort.getRooms(this.date, this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl) + .then(res => { + if(res?.data?.retval) this.setupData(res.data.retval) + }) + }, + handleUuidDefined(uuid) { + this.tabulatorUuid = uuid + }, + search(){ + this.loadRooms() + }, + setRoute(val) { + // TODO: router push + }, + dateFormat(date) { + const day = date.getDate(); + const month = date.getMonth() + 1; + const year = date.getFullYear(); + return `${day}.${month}.${year}` + }, + timeFormat(date) { + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; + }, + async setupMounted() { + this.tableBuiltPromise = new Promise(this.tableResolve) + await this.tableBuiltPromise + + this.loadRoomTypes() + this.loadRooms() + + const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' + const tableDataSet = document.getElementById('filterTableDataset' + tableID); + if(!tableDataSet) return + const rect = tableDataSet.getBoundingClientRect(); + + const h = window.visualViewport.height - rect.top - 100 + if(this.$refs.raumsucheTable) { + this.$refs.raumsucheTable.$refs.table.style.setProperty('height', h+'px') + } + + } + }, + watch: { + + }, + computed: { + + }, + created() { + + }, + mounted() { + this.setupMounted() + }, + template: ` +

{{$p.t('lvplan/raumsuche')}}

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + + `, +}; diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index fb71696bf..f952262f8 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -20184,6 +20184,26 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'rauminfo', + 'phrase' => 'raum_kurzbz', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Raum Kurzbezeichnung", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Room Shortname", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'rauminfo', @@ -20204,6 +20224,66 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'rauminfo', + 'phrase' => 'roomSearch', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Räume Suchen", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Search Rooms", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'rauminfo', + 'phrase' => 'anzahlPersonen', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Anzahl Person", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Number of People", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'rauminfo', + 'phrase' => 'roomReservations', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Raum Reservierungen", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Room Reservations", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'rauminfo',