diff --git a/application/config/search.php b/application/config/search.php new file mode 100644 index 000000000..ac6189527 --- /dev/null +++ b/application/config/search.php @@ -0,0 +1,664 @@ + 'person_id', + 'table' => 'public.tbl_person', + 'searchfields' => [ + 'uid' => [ + 'comparison' => 'equals', + 'field' => 'uid', + 'join' => [ + 'table' => "public.tbl_benutzer", + 'using' => "person_id" + ], + '1-n' => true + ], + 'vorname' => [ + 'alias' => ['firstname'], + 'comparison' => 'similar', + 'field' => 'vorname' + ], + 'nachname' => [ + 'alias' => ['lastname', 'surename'], + 'comparison' => 'similar', + 'field' => 'nachname' + ], + 'name' => [ + 'comparison' => 'similar', + 'field' => "(vorname || ' ' || nachname)" + ], + 'email' => [ + 'comparison' => 'similar', + 'field' => 'kontakt', + 'join' => [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp = 'email' AND tbl_kontakt.person_id = tbl_person.person_id" + ], + "1-n" => true + ], + 'tel' => [ + 'alias' => ['phone', 'telefon'], + 'comparison' => 'similar', + 'field' => 'kontakt', + 'join' => [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp IN ('telefon', 'so.tel', 'mobil') AND tbl_kontakt.person_id = tbl_person.person_id" + ], + "1-n" => true + ], + 'preid' => [ + 'alias' => ['prestudent_id'], + 'comparison' => 'equal-int', + 'field' => 'prestudent_id', + 'join' => [ + 'table' => "public.tbl_prestudent", + 'using' => "person_id" + ], + '1-n' => true + ], + 'pid' => [ + 'alias' => ['person_id'], + 'comparison' => 'equal-int', + 'field' => 'person_id' + ] + ], + 'resultfields' => [ + "b.uid", + "p.person_id", + "(p.vorname || ' ' || p.nachname) AS name", + "ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email", + "p.foto" + ], + 'resultjoin' => " + JOIN public.tbl_person p USING (person_id) + LEFT JOIN public.tbl_benutzer b USING (person_id)" +]; + +$config['student'] = [ + 'primarykey' => 'student_uid', + 'table' => 'public.tbl_student', + 'searchfields' => [ + 'uid' => [ + 'comparison' => 'equals', + 'field' => 'student_uid' + ], + 'vorname' => [ + 'alias' => ['firstname'], + 'comparison' => 'similar', + 'field' => 'vorname', + 'join' => [ + [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ], + [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'nachname' => [ + 'alias' => ['lastname', 'surename'], + 'comparison' => 'similar', + 'field' => 'nachname', + 'join' => [ + [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ], + [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'name' => [ + 'comparison' => 'similar', + 'field' => "(vorname || ' ' || nachname)", + 'join' => [ + [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ], + [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'email' => [ + 'comparison' => 'similar', + 'field' => 'kontakt', + 'join' => [ + [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ], + [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp = 'email' AND tbl_kontakt.person_id = tbl_prestudent.person_id" + ] + ], + "1-n" => true + ], + 'tel' => [ + 'alias' => ['phone', 'telefon'], + 'comparison' => 'similar', + 'field' => 'kontakt', + 'join' => [ + [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ], + [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp IN ('telefon', 'so.tel', 'mobil') AND tbl_kontakt.person_id = tbl_prestudent.person_id" + ] + ], + "1-n" => true + ], + 'stg' => [ + 'alias' => ['studiengang'], + 'comparison' => 'equals', + 'field' => "typ || kurzbz", + 'join' => [ + [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ], + [ + 'table' => "public.tbl_studiengang", + 'on' => "tbl_studiengang.studiengang_kz = tbl_prestudent.studiengang_kz" + ] + ] + ], + 'preid' => [ + 'alias' => ['prestudent_id'], + 'comparison' => 'equal-int', + 'field' => 'prestudent_id' + ], + 'pid' => [ + 'alias' => ['person_id'], + 'comparison' => 'equal-int', + 'field' => 'person_id', + 'join' => [ + 'table' => "public.tbl_prestudent", + 'using' => "prestudent_id" + ] + ] + ], + 'resultfields' => [ + "s.student_uid AS uid", + "s.matrikelnr", + "p.person_id", + "(p.vorname || ' ' || p.nachname) AS name", + "ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email", + "p.foto" + ], + 'resultjoin' => " + JOIN public.tbl_student s USING (student_uid) + JOIN public.tbl_benutzer b ON(b.uid = s.student_uid) + JOIN public.tbl_person p USING(person_id)" +]; + +// TODO(chris): "ref" +$config['prestudent'] = [ + 'primarykey' => 'prestudent_id', + 'table' => 'public.tbl_prestudent', + 'searchfields' => [ + 'uid' => [ + 'comparison' => 'equals', + 'field' => 'student_uid', + 'join' => [ + 'table' => "public.tbl_student", + 'using' => "prestudent_id" + ] + ], + 'vorname' => [ + 'alias' => ['firstname'], + 'comparison' => 'similar', + 'field' => 'vorname', + 'join' => [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ], + 'nachname' => [ + 'alias' => ['lastname', 'surename'], + 'comparison' => 'similar', + 'field' => 'nachname', + 'join' => [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ], + 'name' => [ + 'comparison' => 'similar', + 'field' => "(vorname || ' ' || nachname)", + 'join' => [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ], + 'email' => [ + 'comparison' => 'similar', + 'field' => 'kontakt', + 'join' => [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp = 'email' AND tbl_kontakt.person_id = tbl_prestudent.person_id" + ], + "1-n" => true + ], + 'tel' => [ + 'alias' => ['phone', 'telefon'], + 'comparison' => 'similar', + 'field' => 'kontakt', + 'join' => [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp IN ('telefon', 'so.tel', 'mobil') AND tbl_kontakt.person_id = tbl_prestudent.person_id" + ], + "1-n" => true + ], + 'stg' => [ + 'alias' => ['studiengang'], + 'comparison' => 'equals', + 'field' => "typ || kurzbz", + 'join' => [ + 'table' => "public.tbl_studiengang", + 'using' => "studiengang_kz" + ] + ], + 'preid' => [ + 'alias' => ['prestudent_id'], + 'comparison' => 'equal-int', + 'field' => 'prestudent_id' + ], + 'pid' => [ + 'alias' => ['person_id'], + 'comparison' => 'equal-int', + 'field' => 'person_id', + 'join' => [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'resultfields' => [ + "ps.prestudent_id", + "ps.studiengang_kz", + "s.matrikelnr", + "p.person_id", + "b.uid", + "(p.vorname || ' ' || p.nachname) AS name", + "ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email", + "p.foto", + "UPPER(sg.typ || sg.kurzbz) AS stg_kuerzel", + "sg.bezeichnung", + "( + SELECT bezeichnung_mehrsprachig[(TABLE lang)] + FROM public.tbl_status + WHERE status_kurzbz = public.get_rolle_prestudent(ps.prestudent_id, NULL) + LIMIT 1 + ) as status" + ], + 'resultjoin' => " + LEFT JOIN public.tbl_prestudent ps USING (prestudent_id) + LEFT JOIN public.tbl_student s ON (ps.prestudent_id = s.prestudent_id) + LEFT JOIN public.tbl_benutzer b ON (b.uid = s.student_uid) + JOIN public.tbl_person p ON (p.person_id = ps.person_id) + LEFT JOIN public.tbl_studiengang sg ON (sg.studiengang_kz = ps.studiengang_kz)" +]; + +$config['employee'] = [ + 'alias' => ['ma', 'mitarbeiter'], + 'primarykey' => 'mitarbeiter_uid', + 'table' => 'public.tbl_mitarbeiter', + 'searchfields' => [ + 'uid' => [ + 'alias' => ['mitarbeiter_uid'], + 'comparison' => 'equals', + 'field' => "mitarbeiter_uid" + ], + 'vorname' => [ + 'alias' => ['firstname'], + 'comparison' => 'similar', + 'field' => "vorname", + 'join' => [ + [ + 'table' => "public.tbl_benutzer", + 'on' => "uid = mitarbeiter_uid" + ], + [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'nachname' => [ + 'alias' => ['lastname', 'surename'], + 'comparison' => 'similar', + 'field' => "nachname", + 'join' => [ + [ + 'table' => "public.tbl_benutzer", + 'on' => "uid = mitarbeiter_uid" + ], + [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'name' => [ + 'comparison' => 'similar', + 'field' => "(vorname || ' ' || nachname)", + 'join' => [ + [ + 'table' => "public.tbl_benutzer", + 'on' => "uid = mitarbeiter_uid" + ], + [ + 'table' => "public.tbl_person", + 'using' => "person_id" + ] + ] + ], + 'email' => [ + 'comparison' => 'similar', + 'field' => "COALESCE(alias, uid) || '" . '@' . DOMAIN . "'", + 'join' => [ + 'table' => "public.tbl_benutzer", + 'on' => "uid = mitarbeiter_uid" + ] + ], + 'tel' => [ + 'alias' => ['phone', 'telefon'], + 'comparison' => 'similar', + 'field' => "TRIM(COALESCE(kontakt, '') || ' ' || COALESCE(telefonklappe, ''))", + 'join' => [ + 'table' => "public.tbl_kontakt", + 'on' => "kontakttyp = 'telefon' AND tbl_kontakt.standort_id = tbl_mitarbeiter.standort_id" + ], + "1-n" => true + ], + 'pid' => [ + 'alias' => ['person_id'], + 'comparison' => 'equal-int', + 'field' => "person_id" + ], + 'oe' => [ + 'alias' => ['ou', 'organisationseinheit', 'organisationunit'], + 'comparison' => 'vector', + 'field' => "fts_bezeichnung", + 'join' => [ + [ + 'table' => "public.tbl_benutzerfunktion", + 'on' => "mitarbeiter_uid = uid + AND funktion_kurzbz = 'oezuordnung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW())" + ], + [ + 'table' => "public.tbl_organisationseinheit", + 'using' => "oe_kurzbz" + ] + ], + '1-n' => true + ], + 'kst' => [ + 'comparison' => 'vector', + 'field' => "fts_bezeichnung", + 'join' => [ + [ + 'table' => "public.tbl_benutzerfunktion", + 'on' => "mitarbeiter_uid = uid + AND funktion_kurzbz = 'kstzuordnung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW())" + ], + [ + 'table' => "public.tbl_organisationseinheit", + 'using' => "oe_kurzbz" + ] + ], + '1-n' => true + ] + ], + 'resultfields' => [ + "b.uid", + "p.person_id", + "(p.vorname || ' ' || p.nachname) AS name", + "ARRAY( + SELECT + '[' || ot.bezeichnung || '] ' || o.bezeichnung AS bezeichnung + FROM public.tbl_benutzerfunktion bf + JOIN public.tbl_organisationseinheit o USING(oe_kurzbz) + JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) + WHERE bf.funktion_kurzbz = 'oezuordnung' + AND (bf.datum_von IS NULL OR bf.datum_von <= NOW()) + AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW()) + AND bf.uid = b.uid + GROUP BY o.bezeichnung, ot.bezeichnung + ) AS organisationunit_name", + "COALESCE(b.alias, b.uid) || '" . '@' . DOMAIN . "' AS email", + "TRIM(COALESCE(k.kontakt, '') || ' ' || COALESCE(m.telefonklappe, '')) AS phone", + "'" . base_url("/cis/public/bild.php?src=person&person_id=") . "' || p.person_id AS photo_url", + "ARRAY( + SELECT + '[' || ot.bezeichnung || '] ' || o.bezeichnung AS bezeichnung + FROM public.tbl_benutzerfunktion bf + JOIN public.tbl_organisationseinheit o USING(oe_kurzbz) + JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) + WHERE bf.funktion_kurzbz = 'kstzuordnung' + AND (bf.datum_von IS NULL OR bf.datum_von <= NOW()) + AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW()) + AND bf.uid = b.uid + GROUP BY o.bezeichnung, ot.bezeichnung + ) AS standardkostenstelle" + ], + 'resultjoin' => " + JOIN public.tbl_mitarbeiter m USING (mitarbeiter_uid) + JOIN public.tbl_benutzer b ON (b.uid = m.mitarbeiter_uid) + JOIN public.tbl_person p USING(person_id) + LEFT JOIN ( + SELECT kontakt, standort_id + FROM public.tbl_kontakt + WHERE kontakttyp = 'telefon' + ) k ON (k.standort_id = m.standort_id)" +]; + +$config['unassigned_employee'] = $config['employee']; +$config['unassigned_employee']['alias'] = ['mitarbeiter_ohne_zuordnung']; +$config['unassigned_employee']['prepare'] = "unassigned_employee AS ( + SELECT tbl_mitarbeiter.* + FROM public.tbl_mitarbeiter + LEFT JOIN public.tbl_benutzerfunktion ON ( + uid = mitarbeiter_uid + AND funktion_kurzbz = 'kstzuordnung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + ) + WHERE tbl_benutzerfunktion.bezeichnung IS NULL + UNION + SELECT tbl_mitarbeiter.* + FROM public.tbl_mitarbeiter + LEFT JOIN public.tbl_benutzerfunktion ON ( + uid = mitarbeiter_uid + AND funktion_kurzbz = 'oezuordnung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + ) + WHERE tbl_benutzerfunktion.bezeichnung IS NULL +)"; +$config['unassigned_employee']['table'] = "unassigned_employee"; +$config['unassigned_employee']['searchfields']['tel']['join']['on'] = "kontakttyp = 'telefon' AND tbl_kontakt.standort_id = unassigned_employee.standort_id"; + +$config['organisationunit'] = [ + 'alias' => ['ou', 'organisationseinheit', 'oe'], + 'primarykey' => 'oe_kurzbz', + 'table' => 'public.tbl_organisationseinheit', + 'searchfields' => [ + 'uid' => [ + 'comparison' => 'equals', + 'field' => 'uid', + 'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS ( + SELECT oe_kurzbz, vorname, nachname, uid + FROM public.tbl_benutzerfunktion + JOIN public.tbl_benutzer USING (uid) + JOIN public.tbl_person USING (person_id) + WHERE funktion_kurzbz = 'Leitung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + AND tbl_benutzer.aktiv = TRUE + )", + 'join' => [ + 'table' => "organisationunit_leader", + 'using' => "oe_kurzbz" + ], + '1-n' => true + ], + 'vorname' => [ + 'alias' => ['firstname'], + 'comparison' => 'similar', + 'field' => 'vorname', + 'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS ( + SELECT oe_kurzbz, vorname, nachname, uid + FROM public.tbl_benutzerfunktion + JOIN public.tbl_benutzer USING (uid) + JOIN public.tbl_person USING (person_id) + WHERE funktion_kurzbz = 'Leitung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + AND tbl_benutzer.aktiv = TRUE + )", + 'join' => [ + 'table' => "organisationunit_leader", + 'using' => "oe_kurzbz" + ], + '1-n' => true + ], + 'nachname' => [ + 'alias' => ['lastname', 'surename'], + 'comparison' => 'similar', + 'field' => 'nachname', + 'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS ( + SELECT oe_kurzbz, vorname, nachname, uid + FROM public.tbl_benutzerfunktion + JOIN public.tbl_benutzer USING (uid) + JOIN public.tbl_person USING (person_id) + WHERE funktion_kurzbz = 'Leitung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + AND tbl_benutzer.aktiv = TRUE + )", + 'join' => [ + 'table' => "organisationunit_leader", + 'using' => "oe_kurzbz" + ], + '1-n' => true + ], + 'name' => [ + 'comparison' => 'similar', + 'field' => "(vorname || ' ' || nachname)", + 'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS ( + SELECT oe_kurzbz, vorname, nachname, uid + FROM public.tbl_benutzerfunktion + JOIN public.tbl_benutzer USING (uid) + JOIN public.tbl_person USING (person_id) + WHERE funktion_kurzbz = 'Leitung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + AND tbl_benutzer.aktiv = TRUE + )", + 'join' => [ + 'table' => "organisationunit_leader", + 'using' => "oe_kurzbz" + ], + '1-n' => true + ], + 'oe' => [ + 'alias' => ['ou', 'organisationseinheit', 'organisationunit'], + 'comparison' => 'vector', + 'field' => "fts_bezeichnung" + ], + 'kurzbz' => [ + 'alias' => ['oe_kurzbz'], + 'comparison' => 'equals', + 'field' => "oe_kurzbz" + ] + ], + 'resultfields' => [ + "oe.oe_kurzbz", + "('[' || type.bezeichnung || '] ' || oe.bezeichnung) AS name", + "oe_parent.oe_kurzbz AS parentoe_kurzbz", + "(CASE WHEN oe_parent.bezeichnung IS NOT NULL THEN '[' || type_parent.bezeichnung || '] ' || oe_parent.bezeichnung END) AS parentoe_name", + "ARRAY( + SELECT JSON_BUILD_OBJECT('uid', b.uid, 'vorname', p.vorname, 'nachname', p.nachname, 'name', (p.vorname || ' ' || p.nachname)) + FROM public.tbl_benutzerfunktion bf + JOIN public.tbl_benutzer b USING (uid) + JOIN public.tbl_person p USING (person_id) + WHERE funktion_kurzbz = 'Leitung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + AND b.aktiv = TRUE + AND oe_kurzbz = oe.oe_kurzbz + ) AS leaders", + "( + SELECT COUNT(*) + FROM public.tbl_benutzerfunktion + WHERE funktion_kurzbz = 'oezuordnung' + AND (datum_von IS NULL OR datum_von <= NOW()) + AND (datum_bis IS NULL OR datum_bis >= NOW()) + AND oe_kurzbz = oe.oe_kurzbz + ) AS number_of_people", + "(CASE WHEN oe.mailverteiler THEN oe.oe_kurzbz || '" . '@' . DOMAIN . "' END) AS mailgroup" + ], + 'resultjoin' => " + JOIN public.tbl_organisationseinheit oe + USING (oe_kurzbz) + JOIN public.tbl_organisationseinheittyp type + USING (organisationseinheittyp_kurzbz) + LEFT JOIN public.tbl_organisationseinheit oe_parent + ON (oe_parent.oe_kurzbz = oe.oe_parent_kurzbz) + LEFT JOIN public.tbl_organisationseinheittyp type_parent + ON (oe_parent.organisationseinheittyp_kurzbz = type_parent.organisationseinheittyp_kurzbz)" +]; + +$config['room'] = [ + 'alias' => ['raum'], + 'primarykey' => 'ort_kurzbz', + 'table' => 'public.tbl_ort', + 'searchfields' => [ + 'name' => [ + 'comparison' => 'similar', + 'field' => 'ort_kurzbz' + ] + ], + 'resultfields' => [ + "ort.ort_kurzbz", + "ort.gebteil AS building", + "ort.ausstattung AS equipment", + "ort.stockwerk AS floor", + "ort.dislozierung AS room_number", + "ort.content_id", + "address.ort AS city", + "address.plz AS zip", + "address.strasse AS street", + "ort.max_person", + "ort.arbeitsplaetze AS workplaces" + ], + 'resultjoin' => " + JOIN public.tbl_ort ort + USING (ort_kurzbz) + LEFT JOIN public.tbl_standort + USING (standort_id) + LEFT JOIN public.tbl_adresse address + USING (adresse_id)" +]; diff --git a/application/config/searchfunctions.php b/application/config/searchfunctions.php new file mode 100644 index 000000000..c8244e9a3 --- /dev/null +++ b/application/config/searchfunctions.php @@ -0,0 +1,31 @@ + 4, + 'rank' => "0", + 'compare' => "{field} = {word}", + 'force_integer' => true +]; + +$config['equals'] = [ + 'priority' => 3, + 'rank' => "0", + 'compare' => "LOWER({field}) = {word}" +]; + +$config['similar'] = [ + 'priority' => 2, + 'rank' => "(COALESCE({field}, '') <->> {word})", + 'compare' => "COALESCE({field}, '') %> {word}", + 'compare_boolean' => "COALESCE({field}, '') ILIKE {like:word}" +]; + +$config['vector'] = [ + 'priority' => 1, + 'rank' => "ts_rank_cd({field}, to_tsquery('simple', {word}))", + 'compare' => "to_tsquery('simple', {word}) @@ {field}" +]; + diff --git a/application/controllers/api/frontend/v1/Searchbar.php b/application/controllers/api/frontend/v1/Searchbar.php index 8b383e042..2d5b06063 100644 --- a/application/controllers/api/frontend/v1/Searchbar.php +++ b/application/controllers/api/frontend/v1/Searchbar.php @@ -50,6 +50,7 @@ class Searchbar extends FHCAPI_Controller */ public function search() { + #$searchstrings = ['pid:71995', 'email:eder.iris', 'schnabl', 'schnabl thomas', 'schnabl thomas sarim', 'schnabl -thomas', 'nachname:schnabl -vorname:thomas', 'schnabl thomas -sarim', 'schnabl or hacker', '-ali', '-ali -baba', '-ali -baba -raub', '-ali -honig', '-ali -baba ali', 'hofer martin']; $this->load->library('form_validation'); // Checks if the searchstr and the types parameters are in the POSTed JSON @@ -57,13 +58,17 @@ class Searchbar extends FHCAPI_Controller $this->form_validation->set_rules(self::TYPES_PARAM . '[]', null, 'required'); if (!$this->form_validation->run()) - $this->terminateWithError(SearchBarLib::ERROR_WRONG_JSON, self::ERROR_TYPE_GENERAL); + $this->terminateWithValidationErrors($this->form_validation->error_array()); // Convert to json the result from searchbarlib->search $result = $this->searchbarlib->search($this->input->post(self::SEARCHSTR_PARAM), $this->input->post(self::TYPES_PARAM)); - if (property_exists($result, 'error')) - $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); - $this->terminateWithSuccess($result); + + $data = $this->getDataOrTerminateWithError($result); + + $this->addMeta('time', $result->meta['time']); + $this->addMeta('searchstring', $result->meta['searchstring']); + + $this->terminateWithSuccess($data); } } diff --git a/application/core/FHCAPI_Controller.php b/application/core/FHCAPI_Controller.php index 36388e271..197f4757f 100644 --- a/application/core/FHCAPI_Controller.php +++ b/application/core/FHCAPI_Controller.php @@ -106,10 +106,15 @@ class FHCAPI_Controller extends Auth_Controller $error = []; if (is_array($data)) { - if ($type == self::ERROR_TYPE_VALIDATION) + if ($type == self::ERROR_TYPE_VALIDATION) { $error['messages'] = $data; - else + } elseif (array_is_list($data)) { + foreach ($data as $d) + $this->addError($d, $type); + return; + } else { $error = $data; + } } elseif (is_object($data)) { $error = (array)$data; } else { diff --git a/application/helpers/hlp_common_helper.php b/application/helpers/hlp_common_helper.php index 40aed007c..00c0a1b93 100644 --- a/application/helpers/hlp_common_helper.php +++ b/application/helpers/hlp_common_helper.php @@ -424,6 +424,23 @@ function isValidDate($dateString) } +// ------------------------------------------------------------------------ +// PHP functions that don't exist in older versions +// ------------------------------------------------------------------------ + +/** + * Returns true if the given array is sequential + */ +if (!function_exists('array_is_list')) { + function array_is_list(array $arr) + { + if ($arr === []) { + return true; + } + return array_keys($arr) === range(0, count($arr) - 1); + } +} + // ------------------------------------------------------------------------ // Collection of utility functions for form validation purposes // ------------------------------------------------------------------------ diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index b725f6e90..6547cd107 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -39,6 +39,10 @@ class SearchBarLib private $_ci; // Code igniter instance + private $_searchfunction_priorities = []; + private $_numeric_searchfunctions = []; + private $_allowed_searchfunctions = []; + /** * Gets the CI instance and loads model */ @@ -46,8 +50,22 @@ class SearchBarLib { $this->_ci =& get_instance(); // get code igniter instance - // It is loaded only to have the DB_Model available + // It is loaded only to have the DB functions available $this->_ci->load->model('person/Benutzer_model', 'BenutzerModel'); + + // Load Config + $this->_ci->load->config('search', true); + $this->_ci->load->config('searchfunctions', true); + + $this->_ci->load->library('PhrasesLib', [['search'], null], 'search_phrases'); + + // Precompute helper arrays + foreach ($this->_ci->config->item('searchfunctions') as $key => $arr) { + $this->_searchfunction_priorities[$key] = $arr['priority']; + if ($arr['force_integer'] ?? false) + $this->_numeric_searchfunctions[] = $key; + $this->_allowed_searchfunctions[] = $key; + } } //------------------------------------------------------------------------------------------------------------------ @@ -55,384 +73,612 @@ class SearchBarLib /** * It performes the search of the given search string using the specified search types + * TODO(chris): permissions + * + * @param string $searchstring + * @param array $types (optional) + * + * @return stdClass containing an array with the result on index 0 + * and the overall query time on index 1. */ - public function search($searchstr, $types) + public function search($searchstring, $types = []) { - // Checks if the given parameters are fine - $search = $this->_checkParameters($searchstr, $types); + if (!$types) { + $types = $this->_ci->config->item('search'); + } else { + $tmp = []; + $missing = []; + foreach ($types as $type) { + $typeconfig = $this->_ci->config->item($type, 'search'); + if (!$typeconfig) { + $missing[] = $type; + } else { + $tmp[$type] = $typeconfig; + } + } + if ($missing) { + $p = $this->_ci->search_phrases; + return error(array_map(function ($type) use ($p) { + return $p->t('search', 'error_missing_config', [ + 'type' => $type + ]); // TODO(chris): phrase + }, $missing)); + } + $types = $tmp; + } - // If the check was successful then perform the search - if (isSuccess($search)) $search = $this->_search($searchstr, $types); - return $search; // return the result + // Convert searchstring into array + list($searchArray, $searchstring) = $this->_convertQuery($searchstring, $types); + + + $sql = $this->getDynamicSearchSqls($searchArray, array_keys($types)); + if (isError($sql)) + return $sql; + if (!hasData($sql)) { + $retval = success([]); + $retval->meta = ['time' => 0, 'searchstring' => $searchstring]; + return $retval; + } + + $msc = microtime(true); + $result = $this->_ci->BenutzerModel->execReadOnlyQuery(getData($sql)); + $msc = microtime(true) - $msc; + + if (isError($result)) + return $result; + + $retval = success($result->retval); + $retval->meta = [ + 'time' => $msc, + 'searchstring' => $searchstring + ]; + + return $retval; + } + + /** + * Generates the search query for the given search string and the + * specified search type. + * + * @param array $searchArray + * @param string $table + * + * @return stdClass containing the query string. + */ + public function getDynamicSearchSql($searchArray, $table) + { + $res = $this->checkConfig($table); + if (isError($res)) + return $res; + $table_config = getData($res); + + $sql_with = []; + + $sql_select = $this->prepareDynamicSearchSql($sql_with, $searchArray, $table); + + if (!$sql_select) + return success(""); + + $lang = getUserLanguage(); + + $output = " + WITH lang (index) AS ( + SELECT index + FROM public.tbl_sprache + WHERE sprache=" . $this->_ci->db->escape($lang) . " + LIMIT 1 + )"; + + if ($sql_with) { + $sql_with = array_unique($sql_with); + $output .= ", " . implode(", ", $sql_with); + } + + $other_selects = ""; + if (isset($table_config['resultfields'])) + $other_selects = implode(", ", $table_config['resultfields']); + if ($other_selects) + $other_selects = ", " . $other_selects; + + $output .= " + , q (" . $table_config['primarykey'] . ", rank) AS ( + SELECT " . $table_config['primarykey'] . ", MAX(rank) + FROM (" . implode(" UNION ", $sql_select) . ") q + GROUP BY " . $table_config['primarykey'] . " + ) + SELECT + " . $this->_ci->db->escape($table) . " AS type, + q.rank + " . $other_selects . " + FROM q + " . ($table_config['resultjoin'] ?? "") . " + ORDER BY rank DESC + "; + + return success($output); + } + + /** + * Generates the search query for the given search string and the + * specified search types. + * + * @param array $searchArray + * @param array $types + * + * @return stdClass containing the query string. + */ + public function getDynamicSearchSqls($searchArray, $types) + { + $with = []; + $selects = []; + foreach ($types as $type) { + $res = $this->checkConfig($type); + if (isError($res)) + return $res; + $table_config = getData($res); + + $select = $this->prepareDynamicSearchSql($with, $searchArray, $type); + if (!$select) + continue; + + $with[] = "final_" . $type . " (" . $table_config['primarykey'] . ", rank) AS ( + SELECT " . $table_config['primarykey'] . ", MAX(rank) + FROM (" . implode(" UNION ", $select) . ") q + GROUP BY " . $table_config['primarykey'] . " + )"; + + $other_selects = + $selects[] = " + SELECT + " . $this->_ci->db->escape($type) . " AS type, + rank, + TO_JSONB((SELECT x FROM (SELECT " . implode(", ", $table_config['resultfields'] ?? ['*']) . ") x)) AS data + FROM final_" . $type . ($table_config['resultjoin'] ?? ""); + } + + if (!$selects) + return success(""); + + $with = array_unique($with); + + $lang = getUserLanguage(); + array_unshift($with, "lang (index) AS ( + SELECT index + FROM public.tbl_sprache + WHERE sprache=" . $this->_ci->db->escape($lang) . " + LIMIT 1 + )"); + + return success(" + WITH " . implode(", ", $with) . " + SELECT * + FROM (" . implode(" UNION ", $selects) . ") q + ORDER BY rank DESC + LIMIT 100 + "); + } + + //------------------------------------------------------------------------------------------------------------------ + // Protected methods + + /** + * Check config + * + * @param string $name + * + * @return stdClass + */ + protected function checkConfig($name) + { + $table_config = $this->_ci->config->item($name, 'search'); + + if (!$table_config) + return error($this->_ci->search_phrases->t('search', 'error_missing_config', [ + 'type' => $name + ])); // TODO(chris): phrase + + $errors = []; + if (!isset($table_config['table']) + || !is_string($table_config['table']) + || !$table_config['table'] + ) { + $errors[] = $this->_ci->search_phrases->t('search', 'error_invalid_config', [ + 'type' => $name, + 'field' => 'table' + ]); // TODO(chris): phrase + } + if (!isset($table_config['primarykey']) + || !is_string($table_config['primarykey']) + || !$table_config['primarykey'] + ) { + $errors[] = $this->_ci->search_phrases->t('search', 'error_invalid_config', [ + 'type' => $name, + 'field' => 'primarykey' + ]); + } + if (!isset($table_config['resultfields']) + || !is_array($table_config['resultfields']) + || !$table_config['resultfields'] + ) { + $errors[] = $this->_ci->search_phrases->t('search', 'error_invalid_config', [ + 'type' => $name, + 'field' => 'resultfields' + ]); + } + if (!isset($table_config['searchfields']) + || !is_array($table_config['searchfields']) + || !$table_config['searchfields'] + ) { + $errors[] = $this->_ci->search_phrases->t('search', 'error_invalid_config', [ + 'type' => $name, + 'field' => 'searchfields' + ]); + } else { + foreach ($table_config['searchfields'] as $searchfield => $config) { + if (!isset($config['field']) + || !is_string($config['field']) + || !$config['field'] + ) { + $errors[] = $this->_ci->search_phrases->t('search', 'error_invalid_config_searchfield', [ + 'type' => $name, + 'searchfield' => $searchfield, + 'field' => 'field' + ]); // TODO(chris): phrase + } + if (!isset($config['comparison']) + || !is_string($config['comparison']) + || !in_array($config['comparison'], $this->_allowed_searchfunctions) + ) { + $errors[] = $this->_ci->search_phrases->t('search', 'error_invalid_config_searchfield', [ + 'type' => $name, + 'searchfield' => $searchfield, + 'field' => 'comparison' + ]); + } + } + } + + if ($errors) + return error($errors); + + return success($table_config); + } + + /** + * Generates the with statements for the given search string and the + * specified search type. + * + * @param array &$sqlWith + * @param array $searchArray + * @param string $table + * + * @return string a query string or the name of the prepared select. + */ + protected function prepareDynamicSearchSql(&$sqlWith, $searchArray, $table) + { + $table_config = $this->_ci->config->item($table, 'search'); + + $id_offset = count($sqlWith); + + + $allowed_codes_w_order = ['' => 0, '!' => -1]; + $max = max($this->_searchfunction_priorities); + foreach ($table_config['searchfields'] as $code => $config) { + $allowed_codes_w_order[$code] = $this->_searchfunction_priorities[$config['comparison']]; + $allowed_codes_w_order['!' . $code] = $this->_searchfunction_priorities[$config['comparison']] - $max - 2; + } + + $check_order = $this->_searchfunction_priorities; + uasort($table_config['searchfields'], function ($a, $b) use ($check_order) { + return $check_order[$b['comparison']] - $check_order[$a['comparison']]; + }); + + $integer_functions = $this->_numeric_searchfunctions; + $integer_fields = array_keys(array_filter($table_config['searchfields'], function ($a) use ($integer_functions) { + return in_array($a['comparison'], $integer_functions); + })); + + $only_integer_fields = count($integer_fields) == count($table_config['searchfields']); + + $aliases = []; + foreach ($table_config['searchfields'] as $field => $config) { + if (isset($config['alias'])) { + foreach ($config['alias'] as $alias) { + $aliases[$alias] = $field; + $aliases['!' . $alias] = '!' . $field; + } + } + } + + $sql_select = []; + + if (isset($table_config['prepare'])) { + if (is_array($table_config['prepare'])) + $sqlWith = $table_config['prepare']; + else + $sqlWith[] = $table_config['prepare']; + } + + foreach ($searchArray as $or_search) { + if (isset($or_search['-filter']) && !in_array($table, $or_search['-filter'])) + continue; + unset($or_search['-filter']); + + foreach ($aliases as $alias => $field) { + if (isset($or_search[$alias])) { + $or_search[$field] = array_merge($or_search[$alias], $or_search[$field] ?? []); + unset($or_search[$alias]); + } + } + + // NOTE(chris): early out if not allowed fields are in the search array + $used_codes = array_keys($or_search); + if (count(array_intersect($used_codes, array_keys($allowed_codes_w_order))) != count($used_codes)) + continue; + + // NOTE(chris): expand general excludes to all fields + if (isset($or_search['!'])) { + $not = $or_search['!']; + unset($or_search['!']); + foreach ($table_config['searchfields'] as $code => $config) { + if (isset($or_search['!' . $code])) + $or_search['!' . $code] = array_unique(array_merge($or_search['!' . $code], $not)); + else + $or_search['!' . $code] = $not; + } + } + + // NOTE(chris): early out if all searchfields require an integer and at least one searchword is not a number + if ($only_integer_fields + && isset($or_search[""]) + && $this->_hasAtLeastOneNaN($or_search[""]) + ) { + continue; + } + + $skip = false; + foreach ($integer_fields as $code) { + // NOTE(chris): filter non integer for integer fields + if (isset($or_search['!' . $code])) { + $or_search['!' . $code] = array_filter($or_search['!' . $code], function ($a) { + return is_numeric($a); + }); + if (!$or_search['!' . $code]) + unset($or_search['!' . $code]); + } + // NOTE(chris): early out if a searchword that is not a number is compared to a searchfield that requires an integer + if (isset($or_search[$code]) + && $this->_hasAtLeastOneNaN($or_search[$code]) + ) { + $skip = true; + break; + } + } + if ($skip) + continue; + + // NOTE(chris): sort for performance reasons + uksort($or_search, function ($a, $b) use ($allowed_codes_w_order) { + return $allowed_codes_w_order[$b] - $allowed_codes_w_order[$a]; + }); + + $or_with = []; + $or_select = []; + $or_prepare = []; + + if (substr(key($or_search), 0, 1) == '!') { + // NOTE(chris): only negative searchwords + $sql = []; + foreach ($or_search as $code => $words) { + $code = substr($code, 1); + // NOTE(chris): sort for performance reasons + usort($words, function ($a, $b) { + return strlen($b) - strlen($a); + }); + $field_config = $table_config['searchfields'][$code]; + + if (isset($field_config['prepare'])) { + if (is_array($field_config['prepare'])) + $or_with = array_merge($or_with, $field_config['prepare']); + else + $or_with[] = $field_config['prepare']; + $or_prepare[$code] = $field_config['prepare']; + unset($table_config['searchfields'][$code]['prepare']); + unset($field_config['prepare']); + } + $field_sql = " + SELECT + " . $table_config['table'] . "." . $table_config['primarykey'] . " + FROM " . $table_config['table'] . " + " . $this->_makeJoin($field_config['join']) . " + WHERE "; + // TODO(chris): equals and equal-int could be IN () statement??? + foreach ($words as $word) { + $sql[] = $field_sql . $this->_makeCompareBool($field_config['comparison'], $field_config['field'], $word); + } + } + + $or_select[] = " + SELECT + " . $table_config['primarykey'] . ", + 1.0 AS rank + FROM " . $table_config['table'] . " + WHERE prestudent_id NOT IN (" . implode(" UNION ", $sql) . ")"; + } else { + $current_select = false; + $count = 0; + $skip = false; + foreach ($or_search as $code => $words) { + // NOTE(chris): sort for performance reasons + if ($code && substr($code, 0, 1) == '!') { + usort($words, function ($a, $b) { + return strlen($a) - strlen($b); + }); + } else { + usort($words, function ($a, $b) { + return strlen($b) - strlen($a); + }); + } + if ($code == '') { + foreach ($words as $i => $word) { + $field_sql = []; + foreach ($table_config['searchfields'] as $c => $field_config) { + if (in_array($field_config['comparison'], $integer_functions) && !is_numeric($word)) + continue; + + $word_from = $table_config['table']; + $word_join = ""; + $word_rank = "0"; + if ($current_select) { + $word_from = $current_select; + if ($field_config['field'] != $table_config['primarykey']) { + $word_join .= " " . $this->_makeJoin($table_config); + } + $word_rank = "rank"; + } + if (isset($field_config['prepare'])) { + if (is_array($field_config['prepare'])) + $or_with = array_merge($or_with, $field_config['prepare']); + else + $or_with[] = $field_config['prepare']; + $or_prepare[$c] = $field_config['prepare']; + unset($table_config['searchfields'][$c]['prepare']); + unset($field_config['prepare']); + } + if (isset($field_config['join'])) { + $word_join .= " " . $this->_makeJoin($field_config['join']); + } + $field_sql[] = " + SELECT + " . $word_from . "." . $table_config['primarykey'] . ", + " . $word_rank . " AS w_rank, + " . $this->_makeRank($field_config['comparison'], $field_config['field'], $word) . " AS rank + FROM " . $word_from . " + " . $word_join . " + WHERE " . $this->_makeCompare($field_config['comparison'], $field_config['field'], $word); + } + // NOTE(chris): skip because the word is not numeric but all searchfields require integers + if (!$field_sql) { + $or_with = []; + $or_select = []; + $count = 0; + $skip = true; + foreach ($or_prepare as $k => $v) + $table_config['searchfields'][$k]['prepare'] = $v; + break; + } + + $id = "w" . ($id_offset + count($or_with)); + $or_with[] = " + " . $id . " (" . $table_config['primarykey'] . ", rank) AS ( + SELECT + " . $table_config['primarykey'] . ", + (w_rank + 1.0 - CASE " . + "WHEN MIN(rank) = 0 THEN 0 " . + "ELSE EXP(SUM(LN(CASE WHEN rank = 0 THEN 1 ELSE rank " . + "END))) END) AS rank + FROM (" . implode(' UNION ALL ', $field_sql) . ") " . $id . " + GROUP BY " . $table_config['primarykey'] . ", w_rank + )"; + $current_select = $id; + } + } else { + foreach ($words as $i => $word) { + $where = ""; + $rank = ""; + $jointype = ""; + if (substr($code, 0, 1) == '!') { + $c = substr($code, 1); + $field_config = $table_config['searchfields'][$c]; + + $rank = "1"; + + $jointype = "LEFT"; + + $where = $field_config['field'] . + " IS NULL OR NOT (" . + $this->_makeCompareBool( + $field_config['comparison'], + $field_config['field'], + $word + ) . + ")"; + if ($field_config['1-n'] ?? false) { + $where = "GROUP BY " . + $table_config['primarykey'] . + ", rank HAVING MIN(CASE WHEN " . + $where . + " THEN 1 ELSE 0 END) = 1"; + } else { + $where = "WHERE " . $where; + } + } else { + $field_config = $table_config['searchfields'][$code]; + + $rank = $this->_makeRank($field_config['comparison'], $field_config['field'], $word); + + $where = $this->_makeCompare($field_config['comparison'], $field_config['field'], $word); + $where = "WHERE " . $where; + } + $word_from = $table_config['table']; + $word_join = ""; + $word_rank = ""; + if ($current_select) { + $word_from = $current_select; + if ($field_config['field'] != $table_config['primarykey']) { + $word_join .= " " . $this->_makeJoin($table_config); + } + $word_rank = "rank + "; + } + if (isset($field_config['prepare'])) { + if (is_array($field_config['prepare'])) + $or_with = array_merge($or_with, $field_config['prepare']); + else + $or_with[] = $field_config['prepare']; + $or_prepare[$code] = $field_config['prepare']; + unset($table_config['searchfields'][$code]['prepare']); + unset($field_config['prepare']); + } + if (isset($field_config['join'])) { + $word_join .= " " . $this->_makeJoin($field_config['join'], $jointype); + } + + $id = "w" . ($id_offset + count($or_with)); + $or_with[] = " + " . $id . " (" . $table_config['primarykey'] . ", rank) AS ( + SELECT + " . $word_from . "." . $table_config['primarykey'] . ", + " . $word_rank . $rank . " AS rank + FROM " . $word_from . " + " . $word_join . " + " . $where . " + )"; + $current_select = $id; + } + } + if ($skip) + break; + $count += count($words); + } + + if (!$count || !$current_select) + continue; + + $or_select[] = " + SELECT " . $table_config['primarykey'] . ", rank / " . $count . " AS rank FROM " . $current_select; + } + + $sqlWith = array_merge($sqlWith, $or_with); + $sql_select = array_merge($sql_select, $or_select); + } + + return $sql_select; } //------------------------------------------------------------------------------------------------------------------ // Private methods - /** - * Checks: - * - The given searchstr is a not empty string - * - The given types is a not empty array and contains allowed search types - */ - private function _checkParameters($searchstr, $types) - { - // If searchstr is empty - if (isEmptyString($searchstr)) return error(self::ERROR_WRONG_SEARCHSTR); - - // If types is not an array or it is empty - if (isEmptyArray($types)) return error(self::ERROR_NO_TYPES); - - // If all the elements in types are allowed search types - if (!isEmptyArray(array_diff($types, self::ALLOWED_TYPES))) return error(self::ERROR_WRONG_TYPES); - - return success(); // The check is fine! - } - - /** - * Loops on types and perform the search of that type using searchstr - * Then it collects all the returned data into an array as property of an object - */ - private function _search($searchstr, $types) - { - // Object to be returned - $result = new stdClass(); - $result->data = array(); - - // For each search type - foreach ($types as $type) - { - // Perform the search and then add the result to data - $result->data = array_merge($result->data, $this->{'_'.$type}($searchstr, $type)); - } - - return $result; - } - - private function _mitarbeiter_ohne_zuordnung($searchstr, $type) - { - $dbModel = new DB_Model(); - - $sql = ' - SELECT - \''.$type.'\' AS type, - b.uid AS uid, - p.person_id AS person_id, - p.vorname || \' \' || p.nachname AS name, - ARRAY_AGG(DISTINCT(org.bezeichnung)) AS organisationunit_name, - COALESCE(b.alias, b.uid) || \''.'@'.DOMAIN.'\' AS email, - TRIM(COALESCE(k.kontakt, \'\') || \' \' || COALESCE(m.telefonklappe, \'\')) AS phone, - \''.base_url(self::PHOTO_IMG_URL).'\' || p.person_id AS photo_url, - ARRAY_AGG(DISTINCT(stdkst.bezeichnung)) AS standardkostenstelle - FROM public.tbl_mitarbeiter m - JOIN public.tbl_benutzer b ON(b.uid = m.mitarbeiter_uid) - LEFT JOIN ( - SELECT \'[\' || ot.bezeichnung || \'] \' || o.bezeichnung AS bezeichnung, bf.uid - FROM public.tbl_benutzerfunktion bf - JOIN public.tbl_organisationseinheit o USING(oe_kurzbz) - JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) - WHERE bf.funktion_kurzbz = \'kstzuordnung\' - AND (bf.datum_von IS NULL OR bf.datum_von <= NOW()) - AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW()) - GROUP BY o.bezeichnung, ot.bezeichnung, bf.uid - ) stdkst ON stdkst.uid = b.uid - JOIN public.tbl_person p USING(person_id) - LEFT JOIN ( - SELECT \'[\' || ot.bezeichnung || \'] \' || o.bezeichnung AS bezeichnung, bf.uid - FROM public.tbl_benutzerfunktion bf - JOIN public.tbl_organisationseinheit o USING(oe_kurzbz) - JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) - WHERE bf.funktion_kurzbz = \'oezuordnung\' - AND (bf.datum_von IS NULL OR bf.datum_von <= NOW()) - AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW()) - GROUP BY o.bezeichnung, ot.bezeichnung, bf.uid - ) org ON org.uid = b.uid - LEFT JOIN ( - SELECT kontakt, standort_id - FROM public.tbl_kontakt - WHERE kontakttyp = \'telefon\' - ) k ON(k.standort_id = m.standort_id) - WHERE - (stdkst.bezeichnung IS NULL - OR org.bezeichnung IS NULL) - AND ( - ' . - $this->buildSearchClause( - $dbModel, - array('b.uid', 'p.vorname', 'p.nachname'), - $searchstr - ) . - ' - ) - GROUP BY type, b.uid, p.person_id, name, email, m.telefonklappe, phone - '; - - $employees = $dbModel->execReadOnlyQuery($sql); - - // If something has been found then return it - if (hasData($employees)) return getData($employees); - - // Otherwise return an empty array - return array(); - } - - protected function buildSearchClause(DB_Model $dbModel, array $columns, $searchstr) - { - $document = implode(' || \' \' || ', $columns); - $query = '\'' . implode(':* & ', explode(' ', trim($searchstr))) . ':*\''; - $reversequery = '\'*:' . implode(' & *:', explode(' ', trim($searchstr))) . '\''; - $nospacequery = '\'' . implode('', explode(' ', trim($searchstr))) . ':*\''; - - $searchclause = <<execReadOnlyQuery(' - SELECT - \''.$type.'\' AS type, - b.uid AS uid, - p.person_id AS person_id, - p.vorname || \' \' || p.nachname AS name, - ARRAY_AGG(DISTINCT(org.bezeichnung)) AS organisationunit_name, - COALESCE(b.alias, b.uid) || \''.'@'.DOMAIN.'\' AS email, - TRIM(COALESCE(k.kontakt, \'\') || \' \' || COALESCE(m.telefonklappe, \'\')) AS phone, - \''.base_url(self::PHOTO_IMG_URL).'\' || p.person_id AS photo_url, - ARRAY_AGG(DISTINCT(stdkst.bezeichnung)) AS standardkostenstelle - FROM public.tbl_mitarbeiter m - JOIN public.tbl_benutzer b ON(b.uid = m.mitarbeiter_uid) - JOIN ( - SELECT \'[\' || ot.bezeichnung || \'] \' || o.bezeichnung AS bezeichnung, bf.uid - FROM public.tbl_benutzerfunktion bf - JOIN public.tbl_organisationseinheit o USING(oe_kurzbz) - JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) - WHERE bf.funktion_kurzbz = \'kstzuordnung\' - AND (bf.datum_von IS NULL OR bf.datum_von <= NOW()) - AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW()) - GROUP BY o.bezeichnung, ot.bezeichnung, bf.uid - ) stdkst ON stdkst.uid = b.uid - JOIN public.tbl_person p USING(person_id) - JOIN ( - SELECT \'[\' || ot.bezeichnung || \'] \' || o.bezeichnung AS bezeichnung, bf.uid - FROM public.tbl_benutzerfunktion bf - JOIN public.tbl_organisationseinheit o USING(oe_kurzbz) - JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) - WHERE bf.funktion_kurzbz = \'oezuordnung\' - AND (bf.datum_von IS NULL OR bf.datum_von <= NOW()) - AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW()) - GROUP BY o.bezeichnung, ot.bezeichnung, bf.uid - ) org ON org.uid = b.uid - LEFT JOIN ( - SELECT kontakt, standort_id - FROM public.tbl_kontakt - WHERE kontakttyp = \'telefon\' - ) k ON(k.standort_id = m.standort_id) - WHERE ' . - $this->buildSearchClause( - $dbModel, - array('b.uid', 'p.vorname', 'p.nachname', 'org.bezeichnung', 'stdkst.bezeichnung'), - $searchstr - ) . - ' - GROUP BY type, b.uid, p.person_id, name, email, m.telefonklappe, phone - '); - - // If something has been found then return it - if (hasData($employees)) return getData($employees); - - // Otherwise return an empty array - return array(); - } - - /** - * Seach for organisation units - */ - private function _organisationunit($searchstr, $type) - { - $dbModel = new DB_Model(); - - $ous = $dbModel->execReadOnlyQuery(' - SELECT - \''.$type.'\' AS type, - o.oe_kurzbz AS oe_kurzbz, - \'[\' || ot.bezeichnung || \'] \' || o.bezeichnung AS name, - oParent.oe_kurzbz AS parentoe_kurzbz, - (CASE WHEN oParent.bezeichnung IS NOT NULL THEN \'[\' || otParent.bezeichnung || \'] \' || oParent.bezeichnung END) AS parentoe_name, - ARRAY_AGG(DISTINCT(bfLeader.uid)) AS leader_uid, - ARRAY_AGG(DISTINCT(bfLeader.vorname || \' \' || bfLeader.nachname)) AS leader_name, - COUNT(bfCount.benutzerfunktion_id) AS number_of_people, - (CASE WHEN o.mailverteiler = TRUE THEN o.oe_kurzbz || \''.'@'.DOMAIN.'\' END) AS mailgroup - FROM public.tbl_organisationseinheit o - JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz) - LEFT JOIN public.tbl_organisationseinheit oParent ON(oParent.oe_kurzbz = o.oe_parent_kurzbz) - LEFT JOIN public.tbl_organisationseinheittyp otParent ON(oParent.organisationseinheittyp_kurzbz = otParent.organisationseinheittyp_kurzbz) - LEFT JOIN ( - SELECT benutzerfunktion_id, oe_kurzbz - FROM public.tbl_benutzerfunktion - WHERE funktion_kurzbz = \'oezuordnung\' - AND (datum_von IS NULL OR datum_von <= NOW()) - AND (datum_bis IS NULL OR datum_bis >= NOW()) - ) bfCount ON(bfCount.oe_kurzbz = o.oe_kurzbz) - LEFT JOIN ( - SELECT bf.oe_kurzbz, bf.uid, p.vorname, p.nachname - FROM public.tbl_benutzerfunktion bf - JOIN public.tbl_benutzer b USING(uid) - JOIN public.tbl_person p USING(person_id) - WHERE funktion_kurzbz = \'Leitung\' - AND (datum_von IS NULL OR datum_von <= NOW()) - AND (datum_bis IS NULL OR datum_bis >= NOW()) - AND b.aktiv = TRUE - ) bfLeader ON(bfLeader.oe_kurzbz = o.oe_kurzbz) - WHERE ' . - $this->buildSearchClause( - $dbModel, - array('o.oe_kurzbz', 'o.bezeichnung', 'ot.bezeichnung'), - $searchstr - ) . - ' - GROUP BY type, o.oe_kurzbz, o.bezeichnung, ot.bezeichnung, oParent.oe_kurzbz, oParent.bezeichnung, otParent.bezeichnung - '); - - // If something has been found - if (hasData($ous)) - { - // Loop through the returned dataset - foreach (getData($ous) as $ou) - { - // Create the new property leaders as an empty array - $ou->leaders = array(); - - // Loop through the found leaders for this organisation unit - for ($i = 0; $i < count($ou->leader_uid); $i++) - { - // If a leader exists for this organisationunit and has a name :D - if (!isEmptyString($ou->leader_uid[$i]) && !isEmptyString($ou->leader_name[$i])) - { - // Empty object that will contains the leader uid and name - $leader = new stdClass(); - // Set the properties name and uid - $leader->uid = $ou->leader_uid[$i]; - $leader->name = $ou->leader_name[$i]; - // Add the leader object to the leaders array - $ou->leaders[] = $leader; - } - } - - // Remove the not needed properties leader_uid and leader_name - unset($ou->leader_uid); - unset($ou->leader_name); - } - - // Returns the changed dataset - return getData($ous); - } - - // Otherwise return an empty array - return array(); - } - - /** - * Search for persons - */ - private function _person($searchstr, $type) - { - return array(); - } - - /** - * Search for students - */ - private function _student($searchstr, $type) - { - $dbModel = new DB_Model(); - - $students = $dbModel->execReadOnlyQuery(' - SELECT - \''.$type.'\' AS type, - s.student_uid AS uid, - s.matrikelnr, - p.person_id AS person_id, - p.vorname || \' \' || p.nachname AS name, - k.kontakt as email , - p.foto - FROM public.tbl_student s - JOIN public.tbl_benutzer b ON(b.uid = s.student_uid) - JOIN public.tbl_person p USING(person_id) - LEFT JOIN ( - SELECT kontakt, person_id - FROM public.tbl_kontakt - WHERE kontakttyp = \'email\' - ) as k USING(person_id) - WHERE b.uid ILIKE \'%'.$dbModel->escapeLike($searchstr).'%\' - OR p.vorname ILIKE \'%'.$dbModel->escapeLike($searchstr).'%\' - OR p.nachname ILIKE \'%'.$dbModel->escapeLike($searchstr).'%\' - GROUP BY type, s.student_uid, s.matrikelnr, p.person_id, name, email, p.foto - '); - - // If something has been found then return it - if (hasData($students)) return getData($students); - - // Otherwise return an empty array - return array(); - } - - /** - * Search for prestudents - */ - private function _prestudent($searchstr, $type) - { - $dbModel = new DB_Model(); - - $prestudent = $dbModel->execReadOnlyQuery(' - SELECT - \''.$type.'\' AS type, - ps.prestudent_id, - ps.studiengang_kz, - p.person_id AS person_id, - b.uid, - p.vorname || \' \' || p.nachname AS name, - ( - SELECT kontakt - FROM public.tbl_kontakt - WHERE kontakttyp = \'email\' - AND person_id = p.person_id - LIMIT 1 - ) as email, - p.foto, - sg.bezeichnung - FROM public.tbl_prestudent ps - LEFT JOIN public.tbl_student s USING (prestudent_id) - LEFT JOIN public.tbl_benutzer b ON (b.uid = s.student_uid) - JOIN public.tbl_person p ON (p.person_id = ps.person_id) - LEFT JOIN public.tbl_studiengang sg ON (sg.studiengang_kz = ps.studiengang_kz) - WHERE b.uid ILIKE \'%'.$dbModel->escapeLike($searchstr).'%\' - OR p.vorname ILIKE \'%'.$dbModel->escapeLike($searchstr).'%\' - OR p.nachname ILIKE \'%'.$dbModel->escapeLike($searchstr).'%\' - or cast(ps.prestudent_id as text) ILIKE \'%'.$dbModel->escapeLIKE($searchstr).'%\' - GROUP BY type, b.uid, ps.prestudent_id, ps.studiengang_kz, sg.bezeichnung, s.student_uid, s.matrikelnr, p.person_id, name, email, p.foto - '); - - // If something has been found then return it - if (hasData($prestudent)) return getData($prestudent); - - // Otherwise return an empty array - return array(); - } - /** * Search for documents */ @@ -456,5 +702,324 @@ EOSC; { return array(); } -} + /** + * Checks if an array has at least on non numeric value. + * + * @param array $arr + * + * @return boolean + */ + private function _hasAtLeastOneNaN($arr) + { + foreach ($arr as $value) + if (!is_numeric($value)) + return true; + return false; + } + + /** + * Helper function for getDynamicSearchSql + * + * @param array $join + * @param string $prefix + * + * @return string + */ + private function _makeJoin($join, $prefix = "") + { + if (!is_array($join)) + return ""; + if (!isset($join['table'])) { + $output = []; + foreach ($join as $j) + $output[] = trim($this->_makeJoin($j, $prefix)); + return implode(" ", $output); + } + if (!isset($join['on']) && !isset($join['using']) && !isset($join['primarykey'])) + return ""; + $output = $prefix . " JOIN " . $join['table']; + + if (isset($join['using'])) + return $output . " USING (" . $join['using'] . ")"; + + if (isset($join['primarykey'])) + return $output . " USING (" . $join['primarykey'] . ")"; + + return $output . " ON (" . $join['on'] . ")"; + } + + /** + * Helper function for _makeRank, _makeCompare and _makeCompareBool + * + * @param string $function + * @param string $mode + * @param string $field + * @param string $word + * + * @return string + */ + private function _makeFunction($function, $mode, $field, $word) + { + $searchfunction = $this->_ci->config->item($mode, 'searchfunctions'); + + if (!$searchfunction) + return ""; + $tpl = $searchfunction[$function] ?? ""; + + if (strstr($tpl, '{field}')) + $tpl = str_replace('{field}', $field, $tpl); + + if (strstr($tpl, '{word}')) + $tpl = str_replace('{word}', $this->_ci->db->escape($word), $tpl); + if (strstr($tpl, '{like:word}')) + $tpl = str_replace('{like:word}', "'%" . $this->_ci->db->escapeLike($word) . "%'", $tpl); + + return $tpl; + } + + /** + * Helper function for getDynamicSearchSql + * + * @param string $mode + * @param string $field + * @param string $word + * + * @return string + */ + private function _makeRank($mode, $field, $word) + { + return $this->_makeFunction('rank', $mode, $field, $word); + } + + /** + * Helper function for getDynamicSearchSql + * + * @param string $mode + * @param string $field + * @param string $word + * + * @return string + */ + private function _makeCompare($mode, $field, $word) + { + return $this->_makeFunction('compare', $mode, $field, $word); + } + + /** + * Helper function for getDynamicSearchSql + * + * @param string $mode + * @param string $field + * @param string $word + * + * @return string + */ + private function _makeCompareBool($mode, $field, $word) + { + $searchfunction = $this->_ci->config->item($mode, 'searchfunctions'); + + if (!$searchfunction) + return ""; + $function = isset($searchfunction['compare_boolean']) ? 'compare_boolean' : 'compare'; + return $this->_makeFunction($function, $mode, $field, $word); + } + + /** + * Converts the search string to an array. + * First level should be joined with an OR. + * Second level should be joined with an AND or AND NOT. + * It is an associative array where the key is a code for the field + * which the words should be compared with and the value is the array + * of words. + * Use AND NOT if the first letter in the key is "!". + * Use AND if the first letter in the key is not "!". + * E.g: + * If the key is: + * "": the words should be compared to all fields with AND. + * "!": the words should be compared to all fields with AND NOT. + * "somefield": the words should be compared to the field somefield with + * AND. + * "!somefield": the words should be compared to the field somefield with + * AND NOT. + * + * @param string $searchstring + * @param array $types + * + * @return array + */ + private function _convertQuery($searchstring, $types) + { + $searchAllTypes = count($types) == count($this->_ci->config->item('search')); + $allowedTypes = array_keys($types); + + $currentArray = []; + $outputArray = []; + $cleanStrings = []; + $cleanSearchstring = ''; + $filter = ['+' => [], '-' => []]; + $typeAliases = []; + + $tmp = explode(' ', strtolower($searchstring)); + while ($tmp) { + $chunk = trim(array_shift($tmp)); + if ($chunk == '') + continue; + + if (strpos($chunk, '"') !== false) { + $test = explode('"', $chunk); + if (count($test) > 2) { + $rest = implode('"', array_slice($test, 2)); + if ($rest) { + array_unshift($tmp, $rest); + $chunk = implode('"', array_slice($test, 0, 2)) . '"'; + } + } + if (count($test) == 2) { + while ($tmp && strpos($test[1], '"') === false) { + $test[1] .= ' ' . trim(array_shift($tmp)); + } + if (strpos($test[1], '"') === false) { + $chunk = implode('"', $test) . '"'; + } else { + $test2 = explode('"', $test[1], 2); + $chunk = $test[0] . '"' . $test2[0] . '"'; + if ($test2[1]) { + array_unshift($tmp, $test2[1]); + } + } + } + if (strpos($chunk, ' ') === false) { + $chunk = str_replace('"', '', $chunk); + } + } + + if ($chunk == 'or') { + $this->_convertQueryCleanupOr($currentArray, $cleanStrings, $filter, $searchAllTypes, $allowedTypes); + $filter = ['+' => [], '-' => []]; + if ($currentArray) { + $cleanSearchstring .= ($cleanSearchstring ? ' or ' : '') . implode(' ', $cleanStrings); + $cleanStrings = []; + $outputArray[] = $currentArray; + $currentArray = []; + } + continue; + } + + if ($chunk == ':' || $chunk == '-' || substr($chunk, -1) == ':') + continue; + + if ($chunk[0] == ':' || ($chunk[0] == '-' && $chunk[1] == ':')) { + if (!$typeAliases) { + foreach ($types as $type => $config) { + $typeAliases[$type] = $type; + if (isset($config['alias'])) { + foreach ($config['alias'] as $alias) { + if (!isset($typeAliases[$alias])) + $typeAliases[$alias] = $type; + } + } + } + } + + $test = explode(':', $chunk, 2); + if (isset($typeAliases[$test[1]])) + $chunk = $test[0] . ':' . $typeAliases[$test[1]]; + elseif ($test[0] == '-') + continue; + } + + if (in_array($chunk, $cleanStrings)) + continue; + + $cleanStrings[] = $chunk; + + $chunk = str_replace('"', '', $chunk); + $code = ''; + + if ($chunk[0] == '-') { + $code = '!'; + $chunk = substr($chunk, 1); + } + if (strpos($chunk, ':') !== false) { + $chunk = explode(':', $chunk, 2); + if (!$chunk[0]) { + $filter[$code ? '-' : '+'][] = $chunk[1]; + continue; + } + $code .= $chunk[0]; + $chunk = $chunk[1]; + } + + if (!isset($currentArray[$code])) + $currentArray[$code] = []; + + $currentArray[$code][] = $chunk; + } + + $this->_convertQueryCleanupOr($currentArray, $cleanStrings, $filter, $searchAllTypes, $allowedTypes); + if ($currentArray) { + $cleanSearchstring .= ($cleanSearchstring ? ' or ' : '') . implode(' ', $cleanStrings); + $outputArray[] = $currentArray; + } + return [$outputArray, $cleanSearchstring]; + } + + private function _convertQueryCleanupOr(&$currentArray, &$cleanStrings, $filter, $searchAllTypes, $allowedTypes) + { + if ($filter['+'] && $filter['-']) { + $double = array_intersect($filter['+'], $filter['-']); + if ($double) { + foreach ($double as $type) { + array_splice($cleanStrings, array_search(':' . $type, $cleanStrings), 1); + array_splice($cleanStrings, array_search('-:' . $type, $cleanStrings), 1); + } + $filter['+'] = array_diff($filter['+'], $double); + $filter['-'] = array_diff($filter['-'], $double); + } + if (!$filter['+'] && !$filter['-']) { + // All filters cancel each other out + $currentArray = []; + $cleanStrings = []; + return; + } + if ($filter['+']) { + foreach ($filter['-'] as $type) { + array_splice($cleanStrings, array_search('-:' . $type, $cleanStrings), 1); + } + $filter['-'] = []; + } + } + if ($filter['+']) { + $cleanFilter = array_intersect($allowedTypes, $filter['+']); + if (!$cleanFilter) { + // All filters are forbidden + $currentArray = []; + $cleanStrings = []; + return; + } + $forbiddenFilter = array_diff($cleanFilter, $filter['+']); + foreach ($forbiddenFilter as $type) { + array_splice($cleanStrings, array_search(':' . $type, $cleanStrings), 1); + } + $filter['+'] = $cleanFilter; + } elseif ($filter['-']) { + $filter['+'] = array_diff($allowedTypes, $filter['-']); + if (!$searchAllTypes) { + foreach ($filter['+'] as $type) + $cleanStrings[] = ':' . $type; + foreach ($filter['-'] as $type) + array_splice($cleanStrings, array_search('-:' . $type, $cleanStrings), 1); + } + } else { + if (!$searchAllTypes) { + foreach ($allowedTypes as $type) + $cleanStrings[] = ':' . $type; + } + } + + if ($filter['+']) { + $currentArray['-filter'] = $filter['+']; + } + } +} diff --git a/public/css/components/searchbar.css b/public/css/components/searchbar.css index 2bbaf9d67..61032d227 100644 --- a/public/css/components/searchbar.css +++ b/public/css/components/searchbar.css @@ -92,4 +92,42 @@ .searchbar_inline_ul li { list-style: none; +} + +/* new variant with template/frame */ + +.searchbar-result { + border-bottom: 1px solid lightgrey; + margin-bottom: 1rem; + padding-bottom: 1rem; +} + +.searchbar-actions { + display: flex; + flex-wrap: wrap; + gap: .5rem; + padding: 0; + margin: 1rem 0 0; +} + +.searchbar-square-image { + position: relative; + display: block; + height: 0; + padding-bottom: 100%; +} + +.searchbar-square-image > * { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; +} +.searchbar-square-image img { + object-fit: cover; +} + +.no-margin-paragraphs p { + margin: 0; } \ No newline at end of file diff --git a/public/js/api/search.js b/public/js/api/search.js index 4655d8fa8..c6a5d77a5 100644 --- a/public/js/api/search.js +++ b/public/js/api/search.js @@ -16,9 +16,9 @@ */ export default { - search(searchsettings) { + search(searchsettings, config) { const url = '/api/frontend/v1/searchbar/search'; - return this.$fhcApi.post(url, searchsettings); + return this.$fhcApi.post(url, searchsettings, config); }, searchdummy(searchsettings) { const url = 'public/js/apps/api/dummyapi.php/Search'; diff --git a/public/js/components/searchbar/result/employee.js b/public/js/components/searchbar/result/employee.js new file mode 100644 index 000000000..1c4da075a --- /dev/null +++ b/public/js/components/searchbar/result/employee.js @@ -0,0 +1,59 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + template: ` + +
+
+
Standard-Kostenstelle
+
+
    +
  • {{ stdkst }}
  • +
+ keine +
+
+
+
Organisations-Einheit
+
+
    +
  • {{ oe }}
  • +
+ keine +
+
+
+
EMails
+ +
+
+
Telefon
+ +
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js new file mode 100644 index 000000000..f6b07f3d2 --- /dev/null +++ b/public/js/components/searchbar/result/mergedperson.js @@ -0,0 +1,166 @@ +import TemplateFrame from "./template/frame.js"; +import TemplateAction from "./template/action.js"; + +export default { + components: { + TemplateFrame, + TemplateAction + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + telurl() { + return 'tel:' + this.employee?.phone; + }, + person() { + const person = this.res.list.filter(item => item.type == 'person'); + if (person.length) + return person.pop(); + + const { person_id, name, foto, photo_url, email } = this.res.list[0]; + return { person_id, name, foto, photo_url, email }; + }, + employee() { + const ma = this.res.list.filter(item => [ + 'employee', + 'unassigned_employee' + ].includes(item.type)); + return ma.length ? ma.pop() : null; + }, + students() { + const students = this.res.list.filter(item => item.type == 'prestudent'); + return students.length ? students : null; + }, + foto() { + if (this.person.foto) + return 'data:image/jpeg;base64,' + this.person.foto; + return this.person.photo_url; + }, + emails() { + if (Array.isArray(this.person.email)) + return this.person.email; + return [this.person.email]; + } + }, + template: ` + +
+
+
Person ID
+
+ {{ person.person_id }} +
+
+
+
EMails
+ +
+ + + + +
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/mergedstudent.js b/public/js/components/searchbar/result/mergedstudent.js new file mode 100644 index 000000000..aa5d7739f --- /dev/null +++ b/public/js/components/searchbar/result/mergedstudent.js @@ -0,0 +1,35 @@ +import ResultPrestudent from "./prestudent.js"; +import ResultStudent from "./student.js"; + +export default { + components: { + ResultPrestudent, + ResultStudent + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + prestudent() { + const prestudent = this.res.list.filter(item => item.type == 'prestudent'); + return prestudent.pop(); + } + }, + template: ` + + ` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/organisationunit.js b/public/js/components/searchbar/result/organisationunit.js new file mode 100644 index 000000000..f9b6870e0 --- /dev/null +++ b/public/js/components/searchbar/result/organisationunit.js @@ -0,0 +1,63 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + foto() { + if (this.res.foto) + return 'data:image/jpeg;base64,' + this.res.foto; + return null; + } + }, + template: ` + +
+
+
übergeordnete OrgEinheit
+
+ {{ res.parentoe_name }} +
+
+ +
+
Gruppen-EMail
+ +
+ +
+
Leiter
+
+
    +
  • {{ leader.name }}
  • +
+ N.N. +
+
+ +
+
Mitarbeiter-Anzahl
+
+ {{ res.number_of_people }} +
+
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/person.js b/public/js/components/searchbar/result/person.js new file mode 100644 index 000000000..f8adee56e --- /dev/null +++ b/public/js/components/searchbar/result/person.js @@ -0,0 +1,46 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + foto() { + if (this.res.foto) + return 'data:image/jpeg;base64,' + this.res.foto; + return null; + } + }, + template: ` + +
+
+
Person ID
+
+ {{ res.person_id }} +
+
+
+
EMails
+ +
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/prestudent.js b/public/js/components/searchbar/result/prestudent.js new file mode 100644 index 000000000..8c174d3c0 --- /dev/null +++ b/public/js/components/searchbar/result/prestudent.js @@ -0,0 +1,70 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + foto() { + if (this.res.foto) + return 'data:image/jpeg;base64,' + this.res.foto; + return null; + } + }, + template: ` + +
+
+
Person ID
+
+ {{ res.person_id }} +
+
+
+
EMails
+ +
+
+
Student UID
+
+ {{ res.uid }} +
+
+
+
Matrikelnummer
+
+ {{ res.matrikelnr }} +
+
+
+
Prestudent ID
+
+ {{ res.prestudent_id }} +
+
+
+
Studiengang
+
+ {{ res.bezeichnung }} +
+
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/room.js b/public/js/components/searchbar/result/room.js new file mode 100644 index 000000000..6da1e970a --- /dev/null +++ b/public/js/components/searchbar/result/room.js @@ -0,0 +1,74 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + equipment() { + if (!this.res.equipment) + return ""; + return this.res.equipment.replace(new RegExp('
', 'ig'), ''); + }, + address() { + let address = this.res.zip || ''; + if (this.res.city) + address += (address ? ' ' : '') + this.res.city; + if (this.res.street) + address += (address ? ', ' : '') + this.res.street; + if (this.res.floor) + address += (address ? ' / ' : '') + this.res.floor + ' Stockwerk'; + + return address || 'N/A'; + } + }, + template: ` + +
+
+
Standort
+
+ {{ address }} +
+
+ +
+
Sitzplätze
+
+ + +
+
+ +
+
Gebäude
+
+ {{ res.building }} +
+
+ +
+
Zusatz Informationen
+
+
+
+
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/student.js b/public/js/components/searchbar/result/student.js new file mode 100644 index 000000000..8d8c4894b --- /dev/null +++ b/public/js/components/searchbar/result/student.js @@ -0,0 +1,58 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + foto() { + if (this.res.foto) + return 'data:image/jpeg;base64,' + this.res.foto; + return null; + } + }, + template: ` + +
+
+
Student UID
+
+ {{ res.uid }} +
+
+
+
Person ID
+
+ {{ res.person_id }} +
+
+
+
Matrikelnummer
+
+ {{ res.matrikelnr }} +
+
+
+
EMails
+ +
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/template/action.js b/public/js/components/searchbar/result/template/action.js new file mode 100644 index 000000000..a7654df59 --- /dev/null +++ b/public/js/components/searchbar/result/template/action.js @@ -0,0 +1,28 @@ +export default { + emits: [ 'actionexecuted' ], + props: { + res: Object, + action: Object + }, + computed: { + actionHref() { + if (this.action.type !== 'link') + return 'javascript:void(0);'; + return typeof this.action.action === 'function' + ? this.action.action(this.res) + : this.action.action; + } + }, + methods: { + actionFunc() { + if (this.action.type !== 'function') + return; + this.action.action(this.res); + this.$emit('actionexecuted'); + } + }, + template: ` + + Action + ` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/template/actions.js b/public/js/components/searchbar/result/template/actions.js new file mode 100644 index 000000000..5849594ed --- /dev/null +++ b/public/js/components/searchbar/result/template/actions.js @@ -0,0 +1,26 @@ +import ResultAction from "./action.js"; + +export default { + components: { + ResultAction + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Array + }, + template: ` +
+ + + {{ action.label }} + +
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/template/frame.js b/public/js/components/searchbar/result/template/frame.js new file mode 100644 index 000000000..87078a166 --- /dev/null +++ b/public/js/components/searchbar/result/template/frame.js @@ -0,0 +1,59 @@ +import ResultAction from "./action.js"; +import ResultActions from "./actions.js"; + +export default { + components: { + ResultAction, + ResultActions + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object, + title: String, + image: String, + imageFallback: String + }, + template: ` +
+
+
+ + +
+ +
+
+
+ +
+ + {{ title }} + + + + + + +
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index d23ab2226..54049679e 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -1,168 +1,282 @@ -import person from "./person.js"; -import raum from "./raum.js"; -import employee from "./employee.js"; -import organisationunit from "./organisationunit.js"; -import student from "./student.js"; -import prestudent from "./prestudent.js"; +import ResultPerson from "./result/person.js"; +import ResultStudent from "./result/student.js"; +import ResultPrestudent from "./result/prestudent.js"; +import ResultEmployee from "./result/employee.js"; +import ResultOrganisationunit from "./result/organisationunit.js"; +import ResultRoom from "./result/room.js"; +import ResultMergedperson from "./result/mergedperson.js"; +import ResultMergedstudent from "./result/mergedstudent.js"; + +// TODO(chris): arrays in results export default { - props: [ "searchoptions", "searchfunction" ], - data: function() { - return { - searchtimer: null, - hidetimer: null, - showsettings: false, - searchsettings: { - searchstr: '', - types: [] - }, - showresult: false, - searchresult: [], - searching: false, - error: null - }; - }, - components: { - person: person, - raum: raum, - employee: employee, - organisationunit: organisationunit, - student: student, - prestudent: prestudent - }, - template: ` -
-
- - -
- -
-
- -
-
{{ this.error }}
-
Es wurden keine Ergebnisse gefunden.
- -
-
-
- -
-
- -
- -
- `, - beforeMount: function() { - this.updateSearchOptions(); - }, - methods: { - updateSearchOptions: function() { - this.searchsettings.types = []; - for( const idx in this.searchoptions.types ) { - this.searchsettings.types.push(this.searchoptions.types[idx]); - } - }, - calcSearchResultExtent: function() { - var rect = this.$refs.searchbox.getBoundingClientRect(); + components: { + ResultPerson, + ResultStudent, + ResultPrestudent, + ResultEmployee, + ResultOrganisationunit, + ResultRoom, + ResultMergedperson, + ResultMergedstudent + }, + props: [ "searchoptions", "searchfunction" ], + data() { + return { + searchtimer: null, + hidetimer: null, + showsettings: false, + searchsettings: { + searchstr: '', + types: [] + }, + showresult: false, + searchresult: [], + searching: false, + error: null, + abortController: null, + retry: 5 + }; + }, + beforeMount() { + this.updateSearchOptions(); + }, + methods: { + updateSearchOptions() { + this.searchsettings.types = []; + for (const idx in this.searchoptions.types) { + this.searchsettings.types.push(this.searchoptions.types[idx]); + } + }, + calcSearchResultExtent() { + var rect = this.$refs.searchbox.getBoundingClientRect(); //console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect)); - this.$refs.result.style.height = Math.floor(window.innerHeight * 0.80) + 'px'; - }, - search: function() { - if( this.searchtimer !== null ) { - clearTimeout(this.searchtimer); - } - if( this.searchsettings.searchstr.length >= 2 ) { - this.calcSearchResultExtent(); - this.searchtimer = setTimeout( - this.callsearchapi, - 500 - ); - } else { - this.showresult = false; - } - }, - callsearchapi: function() { - var that = this; - this.error = null; - this.searchresult = []; - this.searching = true; - this.showsearchresult(); - this.searchfunction(this.searchsettings) - .then(function(response) { - if( response.data?.error === 1 ) { - that.error = 'Bei der Suche ist ein Fehler aufgetreten.'; - } else { - that.searchresult = response.data.data; - } - }) - .catch(function(error) { - that.error = 'Bei der Suche ist ein Fehler aufgetreten.' - + ' ' + error.message; - }) - .finally(function() { - that.searching = false; - }); - }, - refreshsearch: function() { - this.search(); - this.togglesettings(); - }, - calcSearchSettingsExtent: function() { - var rect = this.$refs.settingsbutton.getBoundingClientRect(); + this.$refs.result.style.height = Math.floor(window.innerHeight * 0.80) + 'px'; + }, + search() { + if (this.searchtimer !== null) { + clearTimeout(this.searchtimer); + } + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + if (this.searchsettings.searchstr.length >= 2) { + this.calcSearchResultExtent(); + this.searchtimer = setTimeout( + this.callsearchapi, + 500 + ); + } else { + this.showresult = false; + } + }, + callsearchapi() { + this.error = null; + this.searchresult = []; + this.searching = true; + this.showsearchresult(); + + if (this.abortController) + this.abortController.abort(); + this.abortController = new AbortController(); + + this + .searchfunction(this.searchsettings, { signal: this.abortController.signal }) + .then(response => { + if (!response.data) { + this.error = 'Bei der Suche ist ein Fehler aufgetreten.'; + } else { + let res = response.data.map(el => ({...el, ...JSON.parse(el.data)})); + if (this.searchoptions.mergeResults) { + let counter = 0; + let mergeTypes = []; + let mergedType = 'merged'; + let mergeKey = ''; + + switch (this.searchoptions.mergeResults) { + case 'student': + mergeTypes = ['student', 'prestudent']; + mergedType += this.searchoptions.mergeResults; + mergeKey = 'uid'; + break; + case 'person': + mergeTypes = ['person', 'employee', 'unassigned_employee', 'mitarbeiter', 'mitarbeiter_ohne_zuordnung', 'student', 'prestudent']; + mergedType += this.searchoptions.mergeResults; + mergeKey = 'person_id'; + break; + } + + if (mergeTypes.length) { + res = Object.values(res.reduce((a, c) => { + if (!mergeTypes.includes(c.type)) { + a['nomerge' + counter++] = c; + } else if (c[mergeKey] === null) { + a['nomerge' + counter++] = c; + } else if (a[c[mergeKey]] === undefined) { + a[c[mergeKey]] = { + rank: c.rank, + type: mergedType, + list: [c] + }; + } else { + a[c[mergeKey]].list.push(c); + if (c.rank > a[c[mergeKey]].rank) + a[c[mergeKey]].rank = c.rank; + } + return a; + }, {})).sort((a, b) => b.rank - a.rank); + } + } + this.searchresult = res; + } + this.searching = false; + this.retry = 5; + }) + .catch(error => { + if (error.code == "ERR_CANCELED") { + return this.retry = 5; + } + if (error.code == "ECONNABORTED" && this.retry) { + this.retry--; + return this.callsearchapi(); + } + + this.error = 'Bei der Suche ist ein Fehler aufgetreten.' + ' ' + error.message; + this.searching = false; + this.retry = 5; + }); + }, + refreshsearch() { + this.search(); + this.togglesettings(); + }, + calcSearchSettingsExtent() { + var rect = this.$refs.settingsbutton.getBoundingClientRect(); //console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect)); - this.$refs.settings.style.top = Math.floor(rect.bottom + 3) + 'px'; - this.$refs.settings.style.right = Math.floor(window.innerWidth - rect.right) + 'px'; - this.$refs.settings.style.width = Math.floor(window.innerWidth * 0.5) + 'px'; + this.$refs.settings.style.top = Math.floor(rect.bottom + 3) + 'px'; + this.$refs.settings.style.right = Math.floor(window.innerWidth - rect.right) + 'px'; + this.$refs.settings.style.width = Math.floor(window.innerWidth * 0.5) + 'px'; //this.$refs.settings.style.height = Math.floor(window.innerHeight * 0.5) + 'px'; - }, - togglesettings: function() { - this.showsettings = !this.showsettings; - this.calcSearchSettingsExtent(); - }, - hideresult: function() { - this.showresult = false; - window.removeEventListener('resize', this.calcSearchResultExtent); - }, - showsearchresult: function() { - if( this.searchsettings.searchstr.length >= 3 ) { - this.showresult = true; - window.addEventListener('resize', this.calcSearchResultExtent); - } - }, - searchfocusin: function(e) { - e.preventDefault(); - e.stopPropagation(); - if( this.hidetimer !== null ) { - clearTimeout(this.hidetimer); - } - }, - searchfocusout: function(e) { - e.preventDefault(); - e.stopPropagation(); - this.hidetimer = setTimeout( - this.hideresult, - 100 - ); - } - } + }, + togglesettings() { + this.showsettings = !this.showsettings; + this.calcSearchSettingsExtent(); + }, + hideresult() { + this.showresult = false; + window.removeEventListener('resize', this.calcSearchResultExtent); + }, + showsearchresult() { + if (this.searchsettings.searchstr.length >= 3) { + this.showresult = true; + window.addEventListener('resize', this.calcSearchResultExtent); + } + }, + searchfocusin(e) { + e.preventDefault(); + e.stopPropagation(); + if (this.hidetimer !== null) { + clearTimeout(this.hidetimer); + } + }, + searchfocusout(e) { + e.preventDefault(); + e.stopPropagation(); + this.hidetimer = setTimeout( + this.hideresult, + 100 + ); + } + }, + template: ` +
+
+ + +
+ +
+
+ +
+
{{ error }}
+
Es wurden keine Ergebnisse gefunden.
+ +
+
+
+ +
+
+ +
+
` }; diff --git a/public/js/plugin/FhcApi.js b/public/js/plugin/FhcApi.js index 7be1a5b9f..dbf5069b8 100644 --- a/public/js/plugin/FhcApi.js +++ b/public/js/plugin/FhcApi.js @@ -40,6 +40,8 @@ export default { function _clean_return_value(response) { const result = response.data; delete response.data; + if (!result) + return {meta: {response}, data: null}; if (!result.meta) result.meta = {response}; else diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 11880fd55..9bdccecf8 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -58,6 +58,7 @@ require_once('dbupdate_3.4/17513_Entwicklungsteam.php'); require_once('dbupdate_3.4/28575_softwarebereitstellung.php'); require_once('dbupdate_3.4/41150_oe-pfad_db_view.php'); require_once('dbupdate_3.4/44031_stv_favorites.php'); +require_once('dbupdate_3.4/40128_search.php'); // *** Pruefung und hinzufuegen der neuen Attribute und Tabellen echo '

Pruefe Tabellen und Attribute!

'; diff --git a/system/dbupdate_3.4/40128_search.php b/system/dbupdate_3.4/40128_search.php new file mode 100644 index 000000000..2ef8f8b4a --- /dev/null +++ b/system/dbupdate_3.4/40128_search.php @@ -0,0 +1,171 @@ +db_num_rows(@$db->db_query("SELECT 1 +FROM pg_extension WHERE extname = 'pg_trgm' LIMIT 1;"))) +{ + $qry = "CREATE extension pg_trgm;"; + + if (!$db->db_query($qry)) + echo 'Module pg_trgm ' . $db->db_last_error() . '
'; + else + echo 'Module pg_trgm: activated
'; +} + + +// Add additional computed columns +// Add column fts_bezeichnung to public.tbl_organisationseinheit +if (!@$db->db_query("SELECT fts_bezeichnung FROM public.tbl_organisationseinheit LIMIT 1")) +{ + $qry = "ALTER TABLE public.tbl_organisationseinheit ADD COLUMN fts_bezeichnung tsvector;"; + $qry .= "COMMENT ON COLUMN public.tbl_organisationseinheit.fts_bezeichnung IS 'used for search - auto generated w triggers';"; + + if (!$db->db_query($qry)) + echo ' public.tbl_organisationseinheit ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_organisationseinheit: new column "fts_bezeichnung" added
'; +} + +// Add function tr_update_tbl_organisationseinheit_fts_bezeichnung to public +if (!$db->db_num_rows(@$db->db_query("SELECT 1 FROM pg_proc WHERE proname = 'tr_update_tbl_organisationseinheit_fts_bezeichnung' LIMIT 1;"))) +{ + $qry = "CREATE FUNCTION tr_update_tbl_organisationseinheit_fts_bezeichnung() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS + $$ + BEGIN + IF TG_TABLE_NAME = 'tbl_organisationseinheit' THEN + NEW.fts_bezeichnung := to_tsvector('simple', COALESCE((SELECT bezeichnung FROM public.tbl_organisationseinheittyp WHERE organisationseinheittyp_kurzbz = NEW.organisationseinheittyp_kurzbz), '') || ' ' || COALESCE(NEW.bezeichnung, '')); + ELSIF TG_TABLE_NAME = 'tbl_organisationseinheittyp' THEN + UPDATE public.tbl_organisationseinheit SET fts_bezeichnung = to_tsvector('simple', COALESCE(NEW.bezeichnung, '') || ' ' || COALESCE(bezeichnung, '')) WHERE organisationseinheittyp_kurzbz = NEW.organisationseinheittyp_kurzbz; + END IF; + RETURN NEW; + END; + $$"; + + if (!$db->db_query($qry)) + echo ' public.tr_update_tbl_organisationseinheit_fts_bezeichnung ' . $db->db_last_error() . '
'; + else + echo 'public.tr_update_tbl_organisationseinheit_fts_bezeichnung: function created
'; +} + +$update_column = false; +// Add trigger tr_organisationseinheit_update_organisationseinheittyp_kurzbz to public.tbl_organisationseinheit +if (!$db->db_num_rows(@$db->db_query("SELECT 1 FROM information_schema.triggers WHERE event_object_table ='tbl_organisationseinheit' AND trigger_name = 'tr_organisationseinheit_update_organisationseinheittyp_kurzbz' LIMIT 1;"))) +{ + $qry = "CREATE TRIGGER tr_organisationseinheit_update_organisationseinheittyp_kurzbz + BEFORE UPDATE OF organisationseinheittyp_kurzbz OR INSERT + ON public.tbl_organisationseinheit + FOR EACH ROW + EXECUTE FUNCTION tr_update_tbl_organisationseinheit_fts_bezeichnung();"; + + if (!$db->db_query($qry)) + echo ' public.tbl_organisationseinheit ' . $db->db_last_error() . '
'; + else { + echo 'public.tbl_organisationseinheit: trigger "tr_organisationseinheit_update_organisationseinheittyp_kurzbz" created
'; + $update_column = true; + } +} + +// Add trigger tr_organisationseinheittyp_update_bezeichnung to public.tbl_organisationseinheittyp +if (!$db->db_num_rows(@$db->db_query("SELECT 1 FROM information_schema.triggers WHERE event_object_table ='tbl_organisationseinheittyp' AND trigger_name = 'tr_organisationseinheittyp_update_bezeichnung' LIMIT 1;"))) +{ + $qry = "CREATE TRIGGER tr_organisationseinheittyp_update_bezeichnung + BEFORE UPDATE OF bezeichnung + ON public.tbl_organisationseinheittyp + FOR EACH ROW + EXECUTE FUNCTION tr_update_tbl_organisationseinheit_fts_bezeichnung();"; + + if (!$db->db_query($qry)) + echo ' public.tbl_organisationseinheittyp ' . $db->db_last_error() . '
'; + else { + echo 'public.tbl_organisationseinheittyp: trigger "tr_organisationseinheittyp_update_bezeichnung" created
'; + $update_column = true; + } +} + +// Update fts_bezeichnung on tbl_organisationseinheit with new triggers +if ($update_column) +{ + $qry = "UPDATE public.tbl_organisationseinheittyp SET bezeichnung = bezeichnung;"; + + if (!$db->db_query($qry)) + echo ' public.tbl_organisationseinheit ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_organisationseinheit: column "fts_bezeichnung" updated
'; +} + + +// Add Trigram Indexes +// Add index for kontakt to public.tbl_kontakt +if ($result = @$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_kontakt_kontakt_trgm';")) +{ + if ($db->db_num_rows($result) == 0) + { + $qry = "CREATE INDEX idx_tbl_kontakt_kontakt_trgm ON public.tbl_kontakt USING GIN (COALESCE(kontakt, '') gin_trgm_ops);"; + + if (!$db->db_query($qry)) + echo 'public.tbl_kontakt ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_kontakt: added index "idx_tbl_kontakt_kontakt_trgm"
'; + } +} +// Add index for vorname to public.tbl_person +if ($result = @$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_person_vorname_trgm';")) +{ + if ($db->db_num_rows($result) == 0) + { + $qry = "CREATE INDEX idx_tbl_person_vorname_trgm ON public.tbl_person USING GIN (COALESCE(vorname, '') gin_trgm_ops);"; + + if (!$db->db_query($qry)) + echo 'public.tbl_person ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_person: added index "idx_tbl_person_vorname_trgm"
'; + } +} +// Add index for nachname to public.tbl_person +if ($result = @$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_person_nachname_trgm';")) +{ + if ($db->db_num_rows($result) == 0) + { + $qry = "CREATE INDEX idx_tbl_person_nachname_trgm ON public.tbl_person USING GIN (COALESCE(nachname, '') gin_trgm_ops);"; + + if (!$db->db_query($qry)) + echo 'public.tbl_person ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_person: added index "idx_tbl_person_nachname_trgm"
'; + } +} +// Add index for vorname || ' ' || nachname to public.tbl_person +if ($result = @$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_person_name_trgm';")) +{ + if ($db->db_num_rows($result) == 0) + { + $qry = "CREATE INDEX idx_tbl_person_name_trgm ON public.tbl_person USING GIN (COALESCE((vorname || ' ' || nachname), '') gin_trgm_ops);"; + + if (!$db->db_query($qry)) + echo 'public.tbl_person ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_person: added index "idx_tbl_person_name_trgm"
'; + } +} + + +// Add Vector Indexes +// Add index for fts_bezeichnung to public.tbl_organisationseinheit +if (!$db->db_num_rows(@$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_organisationseinheit_fts_bezeichnung_vector' LIMIT 1;"))) +{ + $qry = "CREATE INDEX idx_tbl_organisationseinheit_fts_bezeichnung_vector ON public.tbl_organisationseinheit USING GIN (fts_bezeichnung);"; + + if (!$db->db_query($qry)) + echo 'public.tbl_organisationseinheit ' . $db->db_last_error() . '
'; + else + echo 'public.tbl_organisationseinheit: added index "idx_tbl_organisationseinheit_fts_bezeichnung_vector"
'; +}