From 6375ccde3b9fbaef76644b1102fe55fb3f387bba Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Mon, 22 Jun 2026 13:06:46 +0200 Subject: [PATCH] implemented api endpoint to search coodle survey participants --- .../api/frontend/v1/coodle/Coodle.php | 186 ++++++++++++++++++ public/js/api/factory/coodle.js | 28 +++ .../Cis/Coodle/Components/CoodleSurvey.js | 33 +++- .../CoodleSurvey/CoodleSurveyBasicInfo.js | 20 +- .../CoodleSurvey/CoodleSurveyParticipants.js | 44 +++-- public/js/components/Cis/Coodle/Coodle.js | 13 +- 6 files changed, 286 insertions(+), 38 deletions(-) create mode 100644 application/controllers/api/frontend/v1/coodle/Coodle.php create mode 100644 public/js/api/factory/coodle.js diff --git a/application/controllers/api/frontend/v1/coodle/Coodle.php b/application/controllers/api/frontend/v1/coodle/Coodle.php new file mode 100644 index 000000000..b501f47b6 --- /dev/null +++ b/application/controllers/api/frontend/v1/coodle/Coodle.php @@ -0,0 +1,186 @@ +. + */ + +if (!defined('BASEPATH')) + exit('No direct script access allowed'); + +class Coodle extends FHCAPI_Controller +{ + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'searchParticipants' => self::PERM_LOGGED, + ]); + + $this->load->library('PermissionLib'); + + $this->load->model('person/Benutzer_model', 'BenutzerModel'); + + // todo: remove unnecessary crap + $this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel'); + $this->load->model('person/Person_model', 'PersonModel'); + + + //? put the uid and pid inside the controller for reusability + $this->uid = getAuthUID(); + $this->pid = getAuthPersonID(); + + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + public function searchParticipants() + { + $searchString = $this->input->post("searchString"); + + if (!$searchString || !strlen($searchString)) + $this->terminateWithError("Empty search input!"); + + $users = $this->searchUsersAsParticipants($searchString); + $groups = $this->searchGroupsAsParticipants($searchString); + + $potentialParticipants = array_merge($users, $groups); + + $this->terminateWithSuccess($potentialParticipants); + } + + + // ----------------------------------------------------------------------------------------------------------------- + // Private methods + + private function searchUsersAsParticipants($searchString) + { + $searchStrings = explode(" ", strtolower($searchString)); + + $firstSearchString = $searchStrings[0]; + $remainingSearchStrings = array_slice($searchStrings, 1); + + $query = " + SELECT + 'user' AS type, + benutzer.uid AS uid, + person.vorname || ' ' || person.nachname AS name + FROM public.tbl_person person + JOIN public.tbl_benutzer benutzer ON(benutzer.person_id = person.person_id) + WHERE LOWER(person.vorname || ' ' || person.nachname) LIKE '%" . $firstSearchString . "%' + "; + + if (count($remainingSearchStrings)) { + foreach ($remainingSearchStrings as $remainingSearchString) { + $query .= " AND LOWER(person.vorname || ' ' || person.nachname) LIKE '%" . $remainingSearchString . "%'"; + } + } + + $query .= " LIMIT 25"; + + $dbModel = new DB_Model(); + $usersQueryResult = $dbModel->execReadOnlyQuery($query); + + if (hasData($usersQueryResult)) { + return getData($usersQueryResult); + } else { + return []; + } + + } + + private function searchGroupsAsParticipants($searchString) + { + $searchStrings = explode(" ", strtolower($searchString)); + + $firstSearchString = $searchStrings[0]; + $remainingSearchStrings = array_slice($searchStrings, 1); + + $groupsQuery = " + SELECT + 'group' AS type, + gruppe_kurzbz AS name + FROM public.tbl_gruppe + WHERE LOWER(gruppe_kurzbz) LIKE '%" . $firstSearchString . "%' + "; + + if (count($remainingSearchStrings)) { + foreach ($remainingSearchStrings as $remainingSearchString) { + $groupsQuery .= " AND LOWER(gruppe_kurzbz) LIKE '%" . $remainingSearchString . "%'"; + } + } + + $groupsQuery .= " LIMIT 25"; + + $dbModel = new DB_Model(); + $groupsQueryResult = $dbModel->execReadOnlyQuery($groupsQuery); + // return getData($groupsQueryResult); + + $groups = hasData($groupsQueryResult) ? getData($groupsQueryResult) : []; + if (!count($groups)) + return $groups; + + $groupNames = array_map(function ($group) { + return $group->name; + }, $groups); + + + $userGroupsQuery = " + SELECT + benutzerGruppe.uid as uid, + benutzerGruppe.gruppe_kurzbz as group, + person.vorname || ' ' || person.nachname AS name + FROM public.tbl_benutzergruppe benutzerGruppe + JOIN public.tbl_benutzer benutzer ON(benutzer.uid = benutzerGruppe.uid) + JOIN public.tbl_person person ON(person.person_id = benutzer.person_id) + WHERE gruppe_kurzbz IN ('" . implode("', '", $groupNames) . "') + "; + $userGroupsQueryResult = $dbModel->execReadOnlyQuery($userGroupsQuery); + + $users = hasData($userGroupsQueryResult) ? getData($userGroupsQueryResult) : []; + if (!count($users)) + return []; + + $groupedUsers = []; + + foreach ($users as $user) { + $userData = [ + "uid" => $user->uid, + "name" => $user->name, + ]; + + $group = $user->group; + + if (isset($groupedUsers[$group])) { + $groupedUsers[$group][] = $userData; + } else { + $groupedUsers[$group] = [$userData]; + } + } + + $groups = array_map(function ($group) use ($groupedUsers) { + $group->users = isset($groupedUsers[$group->name]) ? $groupedUsers[$group->name] : []; + return $group; + }, $groups); + $groups = array_filter($groups, function ($group) { + return count($group->users); + }); + return $groups; + } + +} diff --git a/public/js/api/factory/coodle.js b/public/js/api/factory/coodle.js new file mode 100644 index 000000000..9bbc08537 --- /dev/null +++ b/public/js/api/factory/coodle.js @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2025 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 . + */ + +export default { + searchParticipants(searchString) { + return { + method: "post", + url: `/api/frontend/v1/coodle/Coodle/searchParticipants`, + params: { + searchString, + }, + }; + }, +}; diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey.js index e1bbb5f4d..92c1e0c0b 100644 --- a/public/js/components/Cis/Coodle/Components/CoodleSurvey.js +++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey.js @@ -21,6 +21,7 @@ export default { survey: { type: Object | null }, uid: { type: String | null }, }, + emits: ["surveyCreationCanceled"], data() { return { surveyFormData: null, @@ -79,7 +80,7 @@ export default { survey: { async handler() { this.setSurveyFormData(); - if (!this.survey?.id) { + if (!this.$props.survey?.id) { this.isEditInProgress = true; } }, @@ -119,11 +120,9 @@ export default { }; } - this.surveyFormData.participants.forEach( - (participant) => { - participant.isCalendarShown = false; - }, - ); + this.surveyFormData.participants.forEach((participant) => { + participant.isCalendarShown = false; + }); }, parseTimeslot(timeslot) { const timeslotStartsAt = new Date(timeslot.startsAt + " UTC"); @@ -154,9 +153,18 @@ export default { console.log("submitting..."); // todo }, + cancelEditForm() { + this.isEditInProgress = false; + if (!this.$props.survey?.id) { + this.$emit("surveyCreationCanceled"); + } + }, }, created() { this.setSurveyFormData(); + if (!this.$props.survey?.id) { + this.isEditInProgress = true; + } // todo: localize weekdays and months }, template: /*html*/ ` @@ -171,8 +179,13 @@ export default {
+ + {{ "Required inputs are marked with *." }} +
-
+
-
+
diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyBasicInfo.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyBasicInfo.js index 80388a271..582d10d47 100644 --- a/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyBasicInfo.js +++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey/CoodleSurveyBasicInfo.js @@ -14,6 +14,10 @@ export default { return this.$props.survey?.description?.replaceAll("\n", "
"); }, formattedSurveyTimeslotDuration() { + if (this.isNewSurvey) { + return ""; + } + const minutes = this.$props.survey.timeslotDuration % 60; const hours = (this.$props.survey.timeslotDuration - minutes) / 60; let formattedDuration = hours ? hours + " hr " : ""; @@ -56,11 +60,18 @@ export default { } }, surveyCreatorProfileHref() { + if (this.isNewSurvey) { + return ""; + } + return this.$router.resolve({ name: "ProfilView", params: { uid: this.survey?.creator?.uid }, }).href; }, + isNewSurvey() { + return !this.$props.survey?.id; + }, surveyFormData: { get() { return this.$props.surveyFormDataModelValue; @@ -124,7 +135,7 @@ export default {
{{ "Maximum selectable timeslots: " }} - {{ $props.survey.maxSelections}} + {{ $props.survey?.maxSelections}}
@@ -133,22 +144,21 @@ export default {
{{ "Are participants anonymized: " }} - {{ $props.survey.areParticipantsAnonymized ? "yes" : "no" }} + {{ $props.survey?.areParticipantsAnonymized ? "yes" : "no" }}
- +
{{ "Are votes anonymized: " }} - {{ $props.survey.areSelectionsAnonymized ? "yes" : "no" }} + {{ $props.survey?.areSelectionsAnonymized ? "yes" : "no" }}