From 6c08741450589e3b3a82465b9c90f79358de1810 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Fri, 20 Sep 2024 14:31:25 +0200 Subject: [PATCH 001/572] Fist Draft --- application/config/search.php | 664 +++++++++ application/config/searchfunctions.php | 31 + .../controllers/api/frontend/v1/Searchbar.php | 13 +- application/core/FHCAPI_Controller.php | 9 +- application/helpers/hlp_common_helper.php | 17 + application/libraries/SearchBarLib.php | 1307 ++++++++++++----- public/css/components/searchbar.css | 38 + public/js/api/search.js | 4 +- .../components/searchbar/result/employee.js | 59 + .../searchbar/result/mergedperson.js | 166 +++ .../searchbar/result/mergedstudent.js | 35 + .../searchbar/result/organisationunit.js | 63 + .../js/components/searchbar/result/person.js | 46 + .../components/searchbar/result/prestudent.js | 70 + public/js/components/searchbar/result/room.js | 74 + .../js/components/searchbar/result/student.js | 58 + .../searchbar/result/template/action.js | 28 + .../searchbar/result/template/actions.js | 26 + .../searchbar/result/template/frame.js | 59 + public/js/components/searchbar/searchbar.js | 438 ++++-- public/js/plugin/FhcApi.js | 2 + system/dbupdate_3.4.php | 1 + system/dbupdate_3.4/40128_search.php | 171 +++ 23 files changed, 2838 insertions(+), 541 deletions(-) create mode 100644 application/config/search.php create mode 100644 application/config/searchfunctions.php create mode 100644 public/js/components/searchbar/result/employee.js create mode 100644 public/js/components/searchbar/result/mergedperson.js create mode 100644 public/js/components/searchbar/result/mergedstudent.js create mode 100644 public/js/components/searchbar/result/organisationunit.js create mode 100644 public/js/components/searchbar/result/person.js create mode 100644 public/js/components/searchbar/result/prestudent.js create mode 100644 public/js/components/searchbar/result/room.js create mode 100644 public/js/components/searchbar/result/student.js create mode 100644 public/js/components/searchbar/result/template/action.js create mode 100644 public/js/components/searchbar/result/template/actions.js create mode 100644 public/js/components/searchbar/result/template/frame.js create mode 100644 system/dbupdate_3.4/40128_search.php 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"
'; +} From 7174f9cbe08950efb65cf2acd295b41f808f374f Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Fri, 20 Sep 2024 14:38:33 +0200 Subject: [PATCH 002/572] Use Merged Person --- public/js/components/Stv/Studentenverwaltung.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index e2ca8e349..57dd82081 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -91,8 +91,20 @@ export default { }, childactions: [ ] + }, + mergedperson: { + defaultaction: { + type: "link", + action: data => this.$fhcApi.getUri('/studentenverwaltung/person/' + data.person_id) + }, + defaultactionstudent: { + type: "link", + action: data => this.$fhcApi.getUri('/studentenverwaltung/prestudent/' + data.prestudent_id) + }, + childactions: [] } - } + }, + mergeResults: 'person' }, studiengangKz: undefined, studiensemesterKurzbz: this.defaultSemester, From 322544c7fb0746c85934e21d232845f7f38971e6 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Mon, 14 Oct 2024 11:20:46 +0200 Subject: [PATCH 003/572] dynamic image url for searchresults --- application/config/search.php | 15 ++++++++++++--- application/libraries/SearchBarLib.php | 1 - .../components/searchbar/result/mergedperson.js | 12 ++++-------- public/js/components/searchbar/result/person.js | 9 +-------- .../js/components/searchbar/result/prestudent.js | 9 +-------- public/js/components/searchbar/result/student.js | 9 +-------- 6 files changed, 19 insertions(+), 36 deletions(-) diff --git a/application/config/search.php b/application/config/search.php index ac6189527..14b6e89d8 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -74,7 +74,10 @@ $config['person'] = [ "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" + "CASE + WHEN p.foto IS NOT NULL THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto + ELSE NULL END + AS photo_url" ], 'resultjoin' => " JOIN public.tbl_person p USING (person_id) @@ -200,7 +203,10 @@ $config['student'] = [ "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" + "CASE + WHEN p.foto IS NOT NULL THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto + ELSE NULL END + AS photo_url" ], 'resultjoin' => " JOIN public.tbl_student s USING (student_uid) @@ -298,7 +304,10 @@ $config['prestudent'] = [ "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", + "CASE + WHEN p.foto IS NOT NULL THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto + ELSE NULL END + AS photo_url", "UPPER(sg.typ || sg.kurzbz) AS stg_kuerzel", "sg.bezeichnung", "( diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index 6547cd107..c3528978d 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -228,7 +228,6 @@ class SearchBarLib GROUP BY " . $table_config['primarykey'] . " )"; - $other_selects = $selects[] = " SELECT " . $this->_ci->db->escape($type) . " AS type, diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js index f6b07f3d2..09b967b2f 100644 --- a/public/js/components/searchbar/result/mergedperson.js +++ b/public/js/components/searchbar/result/mergedperson.js @@ -20,8 +20,9 @@ export default { 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 }; + // TODO(chris): first one might have not one of these but a later one + const { person_id, name, photo_url, email } = this.res.list[0]; + return { person_id, name, photo_url, email }; }, employee() { const ma = this.res.list.filter(item => [ @@ -34,11 +35,6 @@ export default { 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; @@ -51,7 +47,7 @@ export default { :res="person" :actions="actions" :title="person.name" - :image="foto" + :image="this.person.photo_url" image-fallback="fas fa-user-circle fa-7x" @actionexecuted="$emit('actionexecuted')" > diff --git a/public/js/components/searchbar/result/person.js b/public/js/components/searchbar/result/person.js index f8adee56e..65155a95f 100644 --- a/public/js/components/searchbar/result/person.js +++ b/public/js/components/searchbar/result/person.js @@ -9,20 +9,13 @@ export default { res: Object, actions: Object }, - computed: { - foto() { - if (this.res.foto) - return 'data:image/jpeg;base64,' + this.res.foto; - return null; - } - }, template: ` diff --git a/public/js/components/searchbar/result/prestudent.js b/public/js/components/searchbar/result/prestudent.js index 8c174d3c0..662069b75 100644 --- a/public/js/components/searchbar/result/prestudent.js +++ b/public/js/components/searchbar/result/prestudent.js @@ -9,20 +9,13 @@ export default { res: Object, actions: Object }, - computed: { - foto() { - if (this.res.foto) - return 'data:image/jpeg;base64,' + this.res.foto; - return null; - } - }, template: ` diff --git a/public/js/components/searchbar/result/student.js b/public/js/components/searchbar/result/student.js index 8d8c4894b..93033d08b 100644 --- a/public/js/components/searchbar/result/student.js +++ b/public/js/components/searchbar/result/student.js @@ -9,20 +9,13 @@ export default { res: Object, actions: Object }, - computed: { - foto() { - if (this.res.foto) - return 'data:image/jpeg;base64,' + this.res.foto; - return null; - } - }, template: ` From 123f29a75062d5008fbfdd8b55370f09404ba93e Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Mon, 14 Oct 2024 11:22:20 +0200 Subject: [PATCH 004/572] CMS Search --- application/config/search.php | 94 +++++++++++++++++++ application/config/searchfunctions.php | 2 +- .../controllers/api/frontend/v1/Language.php | 47 ++++++++++ application/libraries/SearchBarLib.php | 6 ++ public/js/api/fhcapifactory.js | 4 +- public/js/api/language.js | 22 +++++ public/js/components/searchbar/result/cms.js | 81 ++++++++++++++++ public/js/components/searchbar/searchbar.js | 34 ++++++- system/dbupdate_3.4/40128_search.php | 15 +++ 9 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 application/controllers/api/frontend/v1/Language.php create mode 100644 public/js/api/language.js create mode 100644 public/js/components/searchbar/result/cms.js diff --git a/application/config/search.php b/application/config/search.php index 14b6e89d8..f52d755dd 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -671,3 +671,97 @@ $config['room'] = [ LEFT JOIN public.tbl_adresse address USING (adresse_id)" ]; + +$config['cms'] = [ + 'primarykey' => 'contentsprache_id', + 'table' => 'cms', + 'prepare' => " + cms_auth (content_id) AS ( + SELECT content_id + FROM campus.tbl_content c + WHERE NOT EXISTS (SELECT 1 FROM campus.tbl_contentgruppe g WHERE g.content_id=c.content_id) + UNION + SELECT content_id + FROM public.vw_gruppen g + JOIN campus.tbl_contentgruppe c USING (gruppe_kurzbz) + WHERE uid = (TABLE auth) + ), + cms_active (content_id, template_kurzbz) AS ( + SELECT content_id, template_kurzbz + FROM cms_auth + JOIN campus.tbl_content USING (content_id) + WHERE aktiv = TRUE + ), + cms_active_redirect (content_id) AS ( + SELECT content_id + FROM cms_active + WHERE template_kurzbz = 'redirect' + ), + cms_active_redirect_linked (content_id) AS ( + SELECT content_id + FROM cms_active_redirect + JOIN campus.tbl_contentsprache USING (content_id) + WHERE LEFT((xpath('string(/content/url)', content))[1]::text, 1) <> '#' + ), + cms_active_others (content_id) AS ( + SELECT content_id + FROM cms_active + WHERE template_kurzbz IN ('contentmittitel', 'contentohnetitel', 'contentmittitel_filterwidget') + ), + cms (contentsprache_id) AS ( + SELECT contentsprache_id + FROM campus.tbl_contentsprache + WHERE content_id IN ( + SELECT content_id + FROM cms_active_redirect_linked + UNION + SELECT content_id + FROM cms_active_others + ) + AND version = campus.get_highest_content_version(content_id) + ) + ", + 'searchfields' => [ + 'content' => [ + 'alias' => ['inhalt'], + 'comparison' => "vector", + 'field' => "(setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))", + 'join' => [ + 'table' => "campus.tbl_contentsprache", + 'using' => "contentsprache_id" + ] + ], + 'content_id' => [ + 'alias' => ['id'], + 'comparison' => "equal-int", + 'field' => "content_id", + 'join' => [ + 'table' => "campus.tbl_contentsprache", + 'using' => "contentsprache_id" + ] + ], + 'lang' => [ + 'alias' => ['language', 'sprache'], + 'comparison' => "equals", + 'field' => "sprache", + 'join' => [ + 'table' => "campus.tbl_contentsprache", + 'using' => "contentsprache_id" + ] + ] + ], + 'resultfields' => [ + "contentsprache.content_id", + "content.template_kurzbz", + "contentsprache.version", + "contentsprache.sprache AS language", + "contentsprache.titel AS title", + "contentsprache.content", + "(xpath('string(/content/url)', contentsprache.content))[1] AS content_url" + ], + 'resultjoin' => " + JOIN campus.tbl_contentsprache contentsprache + USING (contentsprache_id) + JOIN campus.tbl_content content + USING (content_id)" +]; diff --git a/application/config/searchfunctions.php b/application/config/searchfunctions.php index c8244e9a3..898069aed 100644 --- a/application/config/searchfunctions.php +++ b/application/config/searchfunctions.php @@ -25,7 +25,7 @@ $config['similar'] = [ $config['vector'] = [ 'priority' => 1, - 'rank' => "ts_rank_cd({field}, to_tsquery('simple', {word}))", + 'rank' => "ts_rank({field}, to_tsquery('simple', {word}))", 'compare' => "to_tsquery('simple', {word}) @@ {field}" ]; diff --git a/application/controllers/api/frontend/v1/Language.php b/application/controllers/api/frontend/v1/Language.php new file mode 100644 index 000000000..797af4804 --- /dev/null +++ b/application/controllers/api/frontend/v1/Language.php @@ -0,0 +1,47 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * This controller operates between (interface) the JS (GUI) and the back-end + * Provides data to the ajax get calls about languages + * This controller works with JSON calls on the HTTP GET and the output is always JSON + */ +class Language extends FHCAPI_Controller +{ + public function __construct() + { + parent::__construct([ + 'get' => self::PERM_LOGGED + ]); + + // Load models + $this->load->model('system/Sprache_model', 'SpracheModel'); + } + + public function get() + { + $this->SpracheModel->addOrder('sprache'); + + $result = $this->SpracheModel->load(); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } +} diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index c3528978d..782e30843 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -168,6 +168,9 @@ class SearchBarLib FROM public.tbl_sprache WHERE sprache=" . $this->_ci->db->escape($lang) . " LIMIT 1 + ), + auth (uid) AS ( + SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid )"; if ($sql_with) { @@ -248,6 +251,9 @@ class SearchBarLib WHERE sprache=" . $this->_ci->db->escape($lang) . " LIMIT 1 )"); + array_unshift($with, "auth (uid) AS ( + SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid + )"); return success(" WITH " . implode(", ", $with) . " diff --git a/public/js/api/fhcapifactory.js b/public/js/api/fhcapifactory.js index 655bfa409..26ed8847b 100644 --- a/public/js/api/fhcapifactory.js +++ b/public/js/api/fhcapifactory.js @@ -23,6 +23,7 @@ import studstatus from "./studstatus.js"; import stv from "./stv.js"; import notiz from "./notiz.js"; import betriebsmittel from "./betriebsmittel.js"; +import language from "./language.js"; export default { search, @@ -32,5 +33,6 @@ export default { studstatus, stv, notiz, - betriebsmittel + betriebsmittel, + language }; diff --git a/public/js/api/language.js b/public/js/api/language.js new file mode 100644 index 000000000..6a11d193b --- /dev/null +++ b/public/js/api/language.js @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2024 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 { + getAll() { + return this.$fhcApi.get('/api/frontend/v1/language/get'); + } +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/cms.js b/public/js/components/searchbar/result/cms.js new file mode 100644 index 000000000..af2d6b0bb --- /dev/null +++ b/public/js/components/searchbar/result/cms.js @@ -0,0 +1,81 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + inject: [ + 'languages', + 'query' + ], + computed: { + preview() { + if (this.res.template_kurzbz != 'redirect') { + let text = this.res.content.replace(//ig, '').replace(/<[^>]+>/ig, '').replace(/^\s+|\s+$/g, ''); + + if (text.length > 1000) { + // NOTE(chris): focus on searched text! + let lower = text.toLowerCase(); + let firstOccurence = Math.min(this.query.split(' ').reduce((a, c) => { + // NOTE(chris): filter query for words that affects the content field and get the lowest index of them + if (c == 'or') + return a; + let i = c.indexOf(':'); + if (i < 0 || (i > 0 && ['content', 'inhalt'].includes(c.split(':')[0]))) { + let posInText = lower.indexOf(c); + if (posInText >= 0) + a.push(posInText); + } + return a; + }, [])); + + if (firstOccurence) { + if (firstOccurence + 997 >= text.length) { + firstOccurence = text.length - 997; + if (firstOccurence > 0) + return '...' + text.substr(firstOccurence, 997); + } else { + return '...' + text.substr(firstOccurence, 994) + '...'; + } + } + + text = text.substr(0, 997) + '...'; + } + + return text; + } + + let url = this.res.content_url; + if (url.substr(0, 16) == '../index.ci.php/') + url = this.$fhcApi.getUri(url.substr(16)); + else if (url.substr(0, 3) == '../') + url = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/\/+$/, '') + url.substr(2); + return '' + url + ''; + }, + flag() { + if (!this.languages || !this.languages[this.res.language]) + return ""; + return "data:image/jpeg;base64," + this.languages[this.res.language].flagge; + } + }, + template: ` + + +
+
+ No Content +
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 54049679e..8ac64e856 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -4,6 +4,7 @@ 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 ResultCms from "./result/cms.js"; import ResultMergedperson from "./result/mergedperson.js"; import ResultMergedstudent from "./result/mergedstudent.js"; @@ -17,10 +18,17 @@ export default { ResultEmployee, ResultOrganisationunit, ResultRoom, + ResultCms, ResultMergedperson, ResultMergedstudent }, props: [ "searchoptions", "searchfunction" ], + provide() { + return { + languages: Vue.computed(() => this.languages), + query: Vue.computed(() => this.lastQuery) + }; + }, data() { return { searchtimer: null, @@ -35,9 +43,22 @@ export default { searching: false, error: null, abortController: null, - retry: 5 + retry: 0, + languages: null, + lastQuery: '' }; }, + created() { + this.$fhcApi.factory + .language.getAll() + .then(result => { + this.languages = result.data.reduce((a, c) => { + a[c.sprache] = c; + return a; + }, {}); + }) + .catch(this.$fhcAlert.handleSystemError); + }, beforeMount() { this.updateSearchOptions(); }, @@ -82,12 +103,14 @@ export default { this.abortController = new AbortController(); this - .searchfunction(this.searchsettings, { signal: this.abortController.signal }) + .searchfunction(this.searchsettings, { timeout: 50000, 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)})); + this.lastQuery = response.meta.searchstring; + if (this.searchoptions.mergeResults) { let counter = 0; let mergeTypes = []; @@ -131,11 +154,11 @@ export default { this.searchresult = res; } this.searching = false; - this.retry = 5; + this.retry = 0; }) .catch(error => { if (error.code == "ERR_CANCELED") { - return this.retry = 5; + return this.retry = 0; } if (error.code == "ECONNABORTED" && this.retry) { this.retry--; @@ -144,7 +167,7 @@ export default { this.error = 'Bei der Suche ist ein Fehler aufgetreten.' + ' ' + error.message; this.searching = false; - this.retry = 5; + this.retry = 0; }); }, refreshsearch() { @@ -239,6 +262,7 @@ export default { +
Unbekannter Ergebnistyp: '{{ res.type }}'.
diff --git a/system/dbupdate_3.4/40128_search.php b/system/dbupdate_3.4/40128_search.php index 2ef8f8b4a..92e1a10fd 100644 --- a/system/dbupdate_3.4/40128_search.php +++ b/system/dbupdate_3.4/40128_search.php @@ -169,3 +169,18 @@ FROM pg_indexes WHERE indexname = 'idx_tbl_organisationseinheit_fts_bezeichnung_ else echo 'public.tbl_organisationseinheit: added index "idx_tbl_organisationseinheit_fts_bezeichnung_vector"
'; } +// Add index for titel || ' ' || content to campus.tbl_contentsprache +if (!$db->db_num_rows(@$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_contentsprache_fts_titel_content_vector' LIMIT 1;"))) +{ + $qry = " + CREATE INDEX idx_tbl_contentsprache_fts_titel_content_vector + ON campus.tbl_contentsprache + USING GIN ((setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))); + "; + + if (!$db->db_query($qry)) + echo 'campus.tbl_contentsprache ' . $db->db_last_error() . '
'; + else + echo 'campus.tbl_contentsprache: added index "idx_tbl_contentsprache_fts_titel_content_vector"
'; +} From a9a18d1cd43f2ba49b041117f03f6f67aa972df5 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Mon, 28 Oct 2024 11:16:02 +0100 Subject: [PATCH 005/572] Remove deprecated components --- public/js/components/searchbar/action.js | 31 ------- public/js/components/searchbar/actions.js | 28 ------ public/js/components/searchbar/employee.js | 89 ------------------- .../components/searchbar/organisationunit.js | 82 ----------------- public/js/components/searchbar/person.js | 55 ------------ public/js/components/searchbar/prestudent.js | 88 ------------------ public/js/components/searchbar/raum.js | 51 ----------- public/js/components/searchbar/student.js | 80 ----------------- 8 files changed, 504 deletions(-) delete mode 100644 public/js/components/searchbar/action.js delete mode 100644 public/js/components/searchbar/actions.js delete mode 100644 public/js/components/searchbar/employee.js delete mode 100644 public/js/components/searchbar/organisationunit.js delete mode 100644 public/js/components/searchbar/person.js delete mode 100644 public/js/components/searchbar/prestudent.js delete mode 100644 public/js/components/searchbar/raum.js delete mode 100644 public/js/components/searchbar/student.js diff --git a/public/js/components/searchbar/action.js b/public/js/components/searchbar/action.js deleted file mode 100644 index 699d5f8c7..000000000 --- a/public/js/components/searchbar/action.js +++ /dev/null @@ -1,31 +0,0 @@ -export default { - props: { - res: { - type: Object - }, - action: { - type: Object - }, - cssclass: { - type: String, - default: '' - } - }, - emits: [ 'actionexecuted' ], - template: ` - - Action - - `, - methods: { - getactionhref: function() { - return (this.action.type === 'link') ? this.action.action(this.res) - : 'javascript:void(0);'; - }, - execaction: function() { - this.action.action(this.res); - this.$emit('actionexecuted'); - } - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/actions.js b/public/js/components/searchbar/actions.js deleted file mode 100644 index ff9e25e12..000000000 --- a/public/js/components/searchbar/actions.js +++ /dev/null @@ -1,28 +0,0 @@ -import action from "./action.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action - }, - emits: [ 'actionexecuted' ], - template: ` -
    -
  • - - - {{ action.label }} - -
  • -
-
- `, - methods: { - hasicon: function(index) { - return (typeof this.actions[index].icon !== "undefined"); - }, - geticonclass: function(index) { - return this.actions[index].icon; - } - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/employee.js b/public/js/components/searchbar/employee.js deleted file mode 100644 index afb947874..000000000 --- a/public/js/components/searchbar/employee.js +++ /dev/null @@ -1,89 +0,0 @@ -import action from "./action.js"; -import actions from "./actions.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action, - actions: actions - }, - emits: [ 'actionexecuted' ], - template: ` -
- -
-
- - - - -
- -
- - {{ res.name }} - - -
- -
- -
-
Standard-Kostenstelle
-
-
    -
  • {{ stdkst }}
  • -
- keine -
-
- -
-
Organisations-Einheit
-
-
    -
  • {{ oe }}
  • -
- keine -
-
- -
-
EMail
- -
- -
-
Telefon
- -
- -
- - - -
-
- -
- `, - methods: { - }, - computed: { - mailtourl: function() { - return 'mailto:' + this.res.email; - }, - telurl: function() { - return 'tel:' + this.res.phone; - } - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/organisationunit.js b/public/js/components/searchbar/organisationunit.js deleted file mode 100644 index e7b5861e8..000000000 --- a/public/js/components/searchbar/organisationunit.js +++ /dev/null @@ -1,82 +0,0 @@ -import action from "./action.js"; -import actions from "./actions.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action, - actions: actions - }, - emits: [ 'actionexecuted' ], - template: ` -
- -
-
- - - -
- -
- - {{ res.name }} - - -
- -
- -
-
übergeordnete OrgEinheit
-
- {{ res.parentoe_name }} -
-
- -
-
Gruppen-EMail
- -
- -
-
Leiter
-
-
    -
  • {{ leader.name }}
  • -
- N.N. -
-
- -
-
Mitarbeiter-Anzahl
-
- {{ res.number_of_people }} -
-
- -
- - - -
-
- -
- `, - methods: { - }, - computed: { - mailtourl: function() { - return 'mailto:' + this.res.mailgroup; - }, - telurl: function() { - return 'tel:' + this.res.phone; - } - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/person.js b/public/js/components/searchbar/person.js deleted file mode 100644 index 4f267cbd4..000000000 --- a/public/js/components/searchbar/person.js +++ /dev/null @@ -1,55 +0,0 @@ -import action from "./action.js"; -import actions from "./actions.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action, - actions: actions - }, - emits: [ 'actionexecuted' ], - template: ` -
- -
-
- - - - -
- -
- - {{ res.gn }} {{ res.sn }} - - -
- -
-
-
EMail
- -
-
- - - -
-
- -
- `, - methods: { - }, - computed: { - mailtourl: function() { - return 'mailto:' + this.res.mail; - } - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/prestudent.js b/public/js/components/searchbar/prestudent.js deleted file mode 100644 index 0f7429075..000000000 --- a/public/js/components/searchbar/prestudent.js +++ /dev/null @@ -1,88 +0,0 @@ -import action from "./action.js"; -import actions from "./actions.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action, - actions: actions - }, - emits: [ 'actionexecuted' ], - template: ` -
- -
-
- - - - -
- -
- - {{ res.name }} - - -
- -
- -
-
Prestudent_id
-
- {{ res.prestudent_id }} -
-
- -
-
Student_uid
-
- {{ res.uid }} -
-
- -
-
Person_id
-
- {{ res.person_id }} -
-
- -
-
Studiengang
-
- {{ res.bezeichnung }} -
-
- -
-
EMail
- -
- - -
- - - -
-
- -
- `, - methods: { - }, - computed: { - mailtourl: function() { - return 'mailto:' + this.res.email; - } - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/raum.js b/public/js/components/searchbar/raum.js deleted file mode 100644 index 887820d6c..000000000 --- a/public/js/components/searchbar/raum.js +++ /dev/null @@ -1,51 +0,0 @@ -import action from "./action.js"; -import actions from "./actions.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action, - actions: actions - }, - emits: [ 'actionexecuted' ], - template: ` -
- -
-
- - - -
-
- - {{ res.r }} - - -
- -
-
-
Gebäude
-
{{ res.g }}
-
-
-
Stockwerk
-
{{ res.s }}
-
-
-
Raumnummer
-
{{ res.rn }}
-
-
- - - -
-
- -
- `, - methods: { - } -}; \ No newline at end of file diff --git a/public/js/components/searchbar/student.js b/public/js/components/searchbar/student.js deleted file mode 100644 index 73253237c..000000000 --- a/public/js/components/searchbar/student.js +++ /dev/null @@ -1,80 +0,0 @@ -import action from "./action.js"; -import actions from "./actions.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action, - actions: actions - }, - emits: [ 'actionexecuted' ], - template: ` -
- -
-
- - - - -
- -
- - {{ res.name }} - - -
- -
- -
-
Student_uid
-
- {{ res.uid }} -
-
- -
-
Person_id
-
- {{ res.person_id }} -
-
- -
-
Matrikelnummer
-
- {{ res.matrikelnr }} -
-
- -
-
EMail
- -
- -
- - - -
-
- -
- `, - methods: { - }, - computed: { - mailtourl: function() { - return 'mailto:' + this.res.email; - } - } -}; \ No newline at end of file From 084a13316d128ad9a0330473d7cddf301ac10532 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Mon, 28 Oct 2024 11:17:03 +0100 Subject: [PATCH 006/572] Remove duplicate emails --- public/js/components/searchbar/result/mergedperson.js | 2 +- public/js/components/searchbar/result/person.js | 7 ++++++- public/js/components/searchbar/result/prestudent.js | 7 ++++++- public/js/components/searchbar/result/student.js | 7 ++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js index 09b967b2f..8f12540be 100644 --- a/public/js/components/searchbar/result/mergedperson.js +++ b/public/js/components/searchbar/result/mergedperson.js @@ -37,7 +37,7 @@ export default { }, emails() { if (Array.isArray(this.person.email)) - return this.person.email; + return new Set(this.person.email); return [this.person.email]; } }, diff --git a/public/js/components/searchbar/result/person.js b/public/js/components/searchbar/result/person.js index 65155a95f..daa968e72 100644 --- a/public/js/components/searchbar/result/person.js +++ b/public/js/components/searchbar/result/person.js @@ -9,6 +9,11 @@ export default { res: Object, actions: Object }, + computed: { + emails() { + return new Set(this.res.email); + } + }, template: `
EMails
diff --git a/public/js/components/searchbar/result/prestudent.js b/public/js/components/searchbar/result/prestudent.js index 662069b75..4a11ac941 100644 --- a/public/js/components/searchbar/result/prestudent.js +++ b/public/js/components/searchbar/result/prestudent.js @@ -9,6 +9,11 @@ export default { res: Object, actions: Object }, + computed: { + emails() { + return new Set(this.res.email); + } + }, template: `
EMails
diff --git a/public/js/components/searchbar/result/student.js b/public/js/components/searchbar/result/student.js index 93033d08b..3eb401d97 100644 --- a/public/js/components/searchbar/result/student.js +++ b/public/js/components/searchbar/result/student.js @@ -9,6 +9,11 @@ export default { res: Object, actions: Object }, + computed: { + emails() { + return new Set(this.res.email); + } + }, template: `
EMails
From b1f4e5487d0fdd1977000bb91fc0b82fe5eacb3d Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Mon, 28 Oct 2024 11:18:25 +0100 Subject: [PATCH 007/572] Bug: wrong function name --- application/libraries/SearchBarLib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index 782e30843..6d817c20f 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -778,7 +778,7 @@ class SearchBarLib 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); + $tpl = str_replace('{like:word}', "'%" . $this->_ci->db->escape_like_str($word) . "%'", $tpl); return $tpl; } From 963391ec99a061dd96de350afe80da7cb12f9ce2 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Mon, 28 Oct 2024 11:20:51 +0100 Subject: [PATCH 008/572] Bug: counting "with" statements + allowing multi primarykey + allowing "recursive" statement in prepare config --- application/libraries/SearchBarLib.php | 142 +++++++++++++++++-------- 1 file changed, 96 insertions(+), 46 deletions(-) diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index 6d817c20f..54591b11f 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -162,8 +162,14 @@ class SearchBarLib $lang = getUserLanguage(); - $output = " - WITH lang (index) AS ( + $output = "WITH"; + if ($sql_with && $sql_with[0] === 'RECURSIVE') { + $output .= " RECURSIVE"; + array_shift($sql_with); + } + + $output .= " + lang (index) AS ( SELECT index FROM public.tbl_sprache WHERE sprache=" . $this->_ci->db->escape($lang) . " @@ -185,10 +191,10 @@ class SearchBarLib $other_selects = ", " . $other_selects; $output .= " - , q (" . $table_config['primarykey'] . ", rank) AS ( - SELECT " . $table_config['primarykey'] . ", MAX(rank) + , q (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( + SELECT " . $this->_formatPrimarykeys($table_config['primarykey']) . ", MAX(rank) FROM (" . implode(" UNION ", $sql_select) . ") q - GROUP BY " . $table_config['primarykey'] . " + GROUP BY " . $this->_formatPrimarykeys($table_config['primarykey']) . " ) SELECT " . $this->_ci->db->escape($table) . " AS type, @@ -225,10 +231,10 @@ class SearchBarLib if (!$select) continue; - $with[] = "final_" . $type . " (" . $table_config['primarykey'] . ", rank) AS ( - SELECT " . $table_config['primarykey'] . ", MAX(rank) + $with[] = "final_" . $type . " (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( + SELECT " . $this->_formatPrimarykeys($table_config['primarykey']) . ", MAX(rank) FROM (" . implode(" UNION ", $select) . ") q - GROUP BY " . $table_config['primarykey'] . " + GROUP BY " . $this->_formatPrimarykeys($table_config['primarykey']) . " )"; $selects[] = " @@ -242,6 +248,12 @@ class SearchBarLib if (!$selects) return success(""); + $recursive = ""; + if ($with && $with[0] === "RECURSIVE") { + $recursive = "RECURSIVE "; + array_shift($with); + } + $with = array_unique($with); $lang = getUserLanguage(); @@ -256,7 +268,7 @@ class SearchBarLib )"); return success(" - WITH " . implode(", ", $with) . " + WITH " . $recursive . implode(", ", $with) . " SELECT * FROM (" . implode(" UNION ", $selects) . ") q ORDER BY rank DESC @@ -399,10 +411,7 @@ class SearchBarLib $sql_select = []; if (isset($table_config['prepare'])) { - if (is_array($table_config['prepare'])) - $sqlWith = $table_config['prepare']; - else - $sqlWith[] = $table_config['prepare']; + $this->_addPreparesToSqlWith($sqlWith, $table_config['prepare']); } foreach ($searchArray as $or_search) { @@ -484,17 +493,14 @@ class SearchBarLib $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']; + $this->_addPreparesToSqlWith($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'] . " + " . $this->_formatPrimarykeys($table_config['primarykey'], $table_config['table']) . " FROM " . $table_config['table'] . " " . $this->_makeJoin($field_config['join']) . " WHERE "; @@ -506,7 +512,7 @@ class SearchBarLib $or_select[] = " SELECT - " . $table_config['primarykey'] . ", + " . $table_config['table']($table_config['primarykey']) . ", 1.0 AS rank FROM " . $table_config['table'] . " WHERE prestudent_id NOT IN (" . implode(" UNION ", $sql) . ")"; @@ -537,16 +543,13 @@ class SearchBarLib $word_rank = "0"; if ($current_select) { $word_from = $current_select; - if ($field_config['field'] != $table_config['primarykey']) { + if ($this->_needBasicTableJoin($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']; + $this->_addPreparesToSqlWith($or_with, $field_config['prepare']); $or_prepare[$c] = $field_config['prepare']; unset($table_config['searchfields'][$c]['prepare']); unset($field_config['prepare']); @@ -556,7 +559,7 @@ class SearchBarLib } $field_sql[] = " SELECT - " . $word_from . "." . $table_config['primarykey'] . ", + " . $this->_formatPrimarykeys($table_config['primarykey'], $word_from) . ", " . $word_rank . " AS w_rank, " . $this->_makeRank($field_config['comparison'], $field_config['field'], $word) . " AS rank FROM " . $word_from . " @@ -576,15 +579,15 @@ class SearchBarLib $id = "w" . ($id_offset + count($or_with)); $or_with[] = " - " . $id . " (" . $table_config['primarykey'] . ", rank) AS ( + " . $id . " (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( SELECT - " . $table_config['primarykey'] . ", + " . $this->_formatPrimarykeys($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 + GROUP BY " . $this->_formatPrimarykeys($table_config['primarykey']) . ", w_rank )"; $current_select = $id; } @@ -611,7 +614,7 @@ class SearchBarLib ")"; if ($field_config['1-n'] ?? false) { $where = "GROUP BY " . - $table_config['primarykey'] . + $this->_formatPrimarykeys($table_config['primarykey'], $current_select ?: $table_config['table']) . ", rank HAVING MIN(CASE WHEN " . $where . " THEN 1 ELSE 0 END) = 1"; @@ -631,16 +634,13 @@ class SearchBarLib $word_rank = ""; if ($current_select) { $word_from = $current_select; - if ($field_config['field'] != $table_config['primarykey']) { + if ($this->_needBasicTableJoin($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']; + $this->_addPreparesToSqlWith($or_with, $field_config['prepare']); $or_prepare[$code] = $field_config['prepare']; unset($table_config['searchfields'][$code]['prepare']); unset($field_config['prepare']); @@ -651,9 +651,9 @@ class SearchBarLib $id = "w" . ($id_offset + count($or_with)); $or_with[] = " - " . $id . " (" . $table_config['primarykey'] . ", rank) AS ( + " . $id . " (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( SELECT - " . $word_from . "." . $table_config['primarykey'] . ", + " . $this->_formatPrimarykeys($table_config['primarykey'], $word_from) . ", " . $word_rank . $rank . " AS rank FROM " . $word_from . " " . $word_join . " @@ -671,11 +671,12 @@ class SearchBarLib continue; $or_select[] = " - SELECT " . $table_config['primarykey'] . ", rank / " . $count . " AS rank FROM " . $current_select; + SELECT " . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank / " . $count . " AS rank FROM " . $current_select; } $sqlWith = array_merge($sqlWith, $or_with); $sql_select = array_merge($sql_select, $or_select); + $id_offset += count($or_with); } return $sql_select; @@ -685,27 +686,76 @@ class SearchBarLib // Private methods /** - * Search for documents + * Checks if the field is not one of the primarykeys. + * + * @param string $field + * @param array|string $primarykeys + * + * @return boolean */ - private function _document($searchstr, $type) + private function _needBasicTableJoin($field, $primarykeys) { - return array(); + if (!is_array($primarykeys) && strpos($primarykeys, ",") !== false) { + return $field != $primarykeys; + } + if (!is_array($primarykeys)) + $primarykeys = explode(",", $primarykeys); + + foreach ($primarykeys as $key) { + if ($field == trim($key)) + return false; + } + return true; } /** - * Search for CMSs + * Returns comma separated primarykeys. Optionally with table prefix + * + * @param array|string $primarykeys + * @param string $prefix + * + * @return string */ - private function _cms($searchstr, $type) + private function _formatPrimarykeys($primarykeys, $prefix = "") { - return array(); + if (is_array($primarykeys)) { + if ($prefix) + $prefix .= "."; + return $prefix . implode(", " . $prefix, $primarykeys); + } + if (!$prefix) + return $primarykeys; + + return $prefix . "." . implode(", " . $prefix . ".", explode(",", $primarykeys)); } /** - * Search for rooms + * Adds the prepare statement to the sqlWith stack and handles the + * "RECURSIVE" modifier + * + * @param array &$sqlWith + * @param array $prepares + * + * @return void */ - private function _raum($searchstr, $type) + private function _addPreparesToSqlWith(&$sqlWith, $prepares) { - return array(); + $recursive = $sqlWith[0] ?? "" === "RECURSIVE"; + if (!is_array($prepares)) + $prepares = [$prepares]; + + foreach ($prepares as $prep) { + $prep = trim($prep); + if (strtoupper(substr($prep, 0, 10)) === "RECURSIVE ") { + $recursive = true; + $sqlWith[] = substr($prep, 10); + } else { + $sqlWith[] = $prep; + } + } + if ($recursive && $sqlWith[0] !== "RECURSIVE") { + array_unshift($sqlWith, "RECURSIVE"); + } } /** From 8d00a9b4c9b5fa160bbbd7db391a2c45b6399b02 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Thu, 31 Oct 2024 14:22:31 +0100 Subject: [PATCH 009/572] Bugfix --- application/config/search.php | 6 +++++- application/libraries/SearchBarLib.php | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/application/config/search.php b/application/config/search.php index f52d755dd..aa99c17e0 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -400,7 +400,11 @@ $config['employee'] = [ 'pid' => [ 'alias' => ['person_id'], 'comparison' => 'equal-int', - 'field' => "person_id" + 'field' => "person_id", + 'join' => [ + 'table' => "public.tbl_benutzer", + 'on' => "uid = mitarbeiter_uid" + ] ], 'oe' => [ 'alias' => ['ou', 'organisationseinheit', 'organisationunit'], diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index 54591b11f..91abeaebb 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -242,7 +242,8 @@ class SearchBarLib " . $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'] ?? ""); + FROM final_" . $type . " + " . ($table_config['resultjoin'] ?? ""); } if (!$selects) From 0916d3476744fb3504c20a6c92081ed82cb46b77 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Thu, 31 Oct 2024 14:25:27 +0100 Subject: [PATCH 010/572] DMS Search --- application/config/search.php | 135 +++++++++++++++----- public/js/components/searchbar/searchbar.js | 3 + system/dbupdate_3.4/40128_search.php | 15 +++ 3 files changed, 119 insertions(+), 34 deletions(-) diff --git a/application/config/search.php b/application/config/search.php index aa99c17e0..488a1ab3b 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -3,10 +3,6 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); -// TODO(chris): permissions -// TODO(chris): foto - - $config['person'] = [ 'primarykey' => 'person_id', 'table' => 'public.tbl_person', @@ -70,7 +66,7 @@ $config['person'] = [ ] ], 'resultfields' => [ - "b.uid", + "b.uid", // TODO(chris): multiple? "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", @@ -678,7 +674,7 @@ $config['room'] = [ $config['cms'] = [ 'primarykey' => 'contentsprache_id', - 'table' => 'cms', + 'table' => 'campus.tbl_contentsprache', 'prepare' => " cms_auth (content_id) AS ( SELECT content_id @@ -711,47 +707,23 @@ $config['cms'] = [ SELECT content_id FROM cms_active WHERE template_kurzbz IN ('contentmittitel', 'contentohnetitel', 'contentmittitel_filterwidget') - ), - cms (contentsprache_id) AS ( - SELECT contentsprache_id - FROM campus.tbl_contentsprache - WHERE content_id IN ( - SELECT content_id - FROM cms_active_redirect_linked - UNION - SELECT content_id - FROM cms_active_others - ) - AND version = campus.get_highest_content_version(content_id) ) ", 'searchfields' => [ 'content' => [ 'alias' => ['inhalt'], 'comparison' => "vector", - 'field' => "(setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))", - 'join' => [ - 'table' => "campus.tbl_contentsprache", - 'using' => "contentsprache_id" - ] + 'field' => "(setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))" ], 'content_id' => [ 'alias' => ['id'], 'comparison' => "equal-int", - 'field' => "content_id", - 'join' => [ - 'table' => "campus.tbl_contentsprache", - 'using' => "contentsprache_id" - ] + 'field' => "content_id" ], 'lang' => [ 'alias' => ['language', 'sprache'], 'comparison' => "equals", - 'field' => "sprache", - 'join' => [ - 'table' => "campus.tbl_contentsprache", - 'using' => "contentsprache_id" - ] + 'field' => "sprache" ] ], 'resultfields' => [ @@ -767,5 +739,100 @@ $config['cms'] = [ JOIN campus.tbl_contentsprache contentsprache USING (contentsprache_id) JOIN campus.tbl_content content - USING (content_id)" + USING (content_id) + WHERE content_id IN ( + SELECT content_id + FROM cms_active_redirect_linked + UNION + SELECT content_id + FROM cms_active_others + ) + AND version = campus.get_highest_content_version(content_id)" +]; + +$config['dms'] = [ + // TODO(chris): IMPLEMENT + // TODO(chris): TEST + // TODO(chris): project? + 'primarykey' => 'dms_id, version', + 'table' => 'campus.tbl_dms_version', + 'searchfields' => [ + 'keywords' => [ + 'alias' => ['keyword', 'keywords', 'schlagwort', 'schlagworte'], + 'comparison' => "vector", + 'field' => "(to_tsvector('simple', COALESCE(schlagworte, '')))" + ] + ], + 'resultfields' => [ + "v.dms_id", + "v.version", + "v.filename", + "v.mimetype", + "v.name", + "v.beschreibung AS description", + "v.schlagworte AS keywords" + ], + 'resultjoin' => " + JOIN campus.tbl_dms_version v + USING (dms_id, version) + WHERE cis_suche = TRUE + AND version=(SELECT MAX(version) FROM campus.tbl_dms_version WHERE dms_id=v.dms_id) + AND NOT EXISTS ( + SELECT + 1 + FROM + fue.tbl_projekt_dokument p + WHERE p.dms_id = v.dms_id + ) AND ( + NOT EXISTS ( + WITH RECURSIVE categories (kategorie_kurzbz) AS ( + SELECT + kategorie_kurzbz + FROM + campus.tbl_dms c + WHERE c.dms_id = v.dms_id + UNION ALL + SELECT + cat.parent_kategorie_kurzbz AS kategorie_kurzbz + FROM + categories + JOIN campus.tbl_dms_kategorie cat USING (kategorie_kurzbz) + ) + SELECT + 1 + FROM + categories + JOIN campus.tbl_dms_kategorie_gruppe USING (kategorie_kurzbz) + UNION + SELECT + 1 + FROM + categories + JOIN campus.tbl_dms_kategorie USING (kategorie_kurzbz) + WHERE + berechtigung_kurzbz IS NOT NULL + ) OR EXISTS ( + WITH RECURSIVE categories (kategorie_kurzbz) AS ( + SELECT + kategorie_kurzbz + FROM + campus.tbl_dms c + WHERE c.dms_id = v.dms_id + UNION ALL + SELECT + cat.parent_kategorie_kurzbz AS kategorie_kurzbz + FROM + categories + JOIN campus.tbl_dms_kategorie cat USING (kategorie_kurzbz) + ) + SELECT + 1 + FROM + categories + JOIN campus.tbl_dms_kategorie_gruppe USING (kategorie_kurzbz) + JOIN public.tbl_benutzergruppe USING(gruppe_kurzbz) + WHERE + uid = (TABLE auth) + ) + )" ]; diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 8ac64e856..7589cfbef 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -5,6 +5,7 @@ import ResultEmployee from "./result/employee.js"; import ResultOrganisationunit from "./result/organisationunit.js"; import ResultRoom from "./result/room.js"; import ResultCms from "./result/cms.js"; +import ResultDms from "./result/dms.js"; import ResultMergedperson from "./result/mergedperson.js"; import ResultMergedstudent from "./result/mergedstudent.js"; @@ -19,6 +20,7 @@ export default { ResultOrganisationunit, ResultRoom, ResultCms, + ResultDms, ResultMergedperson, ResultMergedstudent }, @@ -263,6 +265,7 @@ export default { +
Unbekannter Ergebnistyp: '{{ res.type }}'.
diff --git a/system/dbupdate_3.4/40128_search.php b/system/dbupdate_3.4/40128_search.php index 92e1a10fd..8ca0fd246 100644 --- a/system/dbupdate_3.4/40128_search.php +++ b/system/dbupdate_3.4/40128_search.php @@ -184,3 +184,18 @@ FROM pg_indexes WHERE indexname = 'idx_tbl_contentsprache_fts_titel_content_vect else echo 'campus.tbl_contentsprache: added index "idx_tbl_contentsprache_fts_titel_content_vector"
'; } +// Add index for schlagworte to campus.tbl_dms_version +if (!$db->db_num_rows(@$db->db_query("SELECT 1 +FROM pg_indexes WHERE indexname = 'idx_tbl_dms_version_fts_schlagworte_vector' LIMIT 1;"))) +{ + $qry = " + CREATE INDEX idx_tbl_dms_version_fts_schlagworte_vector + ON campus.tbl_dms_version + USING GIN ((to_tsvector('simple', COALESCE(schlagworte, '')))); + "; + + if (!$db->db_query($qry)) + echo 'campus.tbl_dms_version ' . $db->db_last_error() . '
'; + else + echo 'campus.tbl_contentsprache: added index "idx_tbl_dms_version_fts_schlagworte_vector"
'; +} From b27564ea0f22d6cf414bfa334e154e13ff325560 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Tue, 5 Nov 2024 09:01:18 +0100 Subject: [PATCH 011/572] Multiple uids in person search --- application/config/search.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/application/config/search.php b/application/config/search.php index 488a1ab3b..8c601e046 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -66,7 +66,7 @@ $config['person'] = [ ] ], 'resultfields' => [ - "b.uid", // TODO(chris): multiple? + "ARRAY( SELECT uid FROM public.tbl_benutzer WHERE person_id = p.person_id ) AS uids", "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", @@ -76,8 +76,7 @@ $config['person'] = [ AS photo_url" ], 'resultjoin' => " - JOIN public.tbl_person p USING (person_id) - LEFT JOIN public.tbl_benutzer b USING (person_id)" + JOIN public.tbl_person p USING (person_id)" ]; $config['student'] = [ @@ -210,7 +209,6 @@ $config['student'] = [ JOIN public.tbl_person p USING(person_id)" ]; -// TODO(chris): "ref" $config['prestudent'] = [ 'primarykey' => 'prestudent_id', 'table' => 'public.tbl_prestudent', @@ -751,9 +749,6 @@ $config['cms'] = [ ]; $config['dms'] = [ - // TODO(chris): IMPLEMENT - // TODO(chris): TEST - // TODO(chris): project? 'primarykey' => 'dms_id, version', 'table' => 'campus.tbl_dms_version', 'searchfields' => [ From 2ff7c9f5823484b6ae929e1fdc6e8ba778ff8862 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Tue, 5 Nov 2024 09:02:09 +0100 Subject: [PATCH 012/572] Dms search result component --- public/js/components/searchbar/result/dms.js | 77 ++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 public/js/components/searchbar/result/dms.js diff --git a/public/js/components/searchbar/result/dms.js b/public/js/components/searchbar/result/dms.js new file mode 100644 index 000000000..74fc9440b --- /dev/null +++ b/public/js/components/searchbar/result/dms.js @@ -0,0 +1,77 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + inject: [ + 'query' + ], + computed: { + icon() { + switch (this.res.mimetype) { + case 'application/pdf': + return 'file-pdf'; + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + case 'application/msword': + return 'file-word'; + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + case 'application/mspowerpoint': + return 'file-powerpoint'; + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/vnd.ms-excel': + return 'file-excel'; + case 'application/x-zip': + case 'application/zip': + return 'file-zipper'; + case 'image/jpeg': + case 'image/gif': + case 'image/png': + return 'file-image'; + default: + return 'file'; + } + } + }, + template: ` + +
+
+
DMS ID
+
+ {{ res.dms_id }} +
+
+
+
Version
+
+ {{ res.version }} +
+
+
+
Keywords
+
+ {{ res.keywords }} +
+
+
+
Description
+
+ {{ res.description }} +
+
+
+
` +}; \ No newline at end of file From 3a13226298323f8bf5bd60e4e903755ad40cf0f9 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Tue, 5 Nov 2024 11:08:07 +0100 Subject: [PATCH 013/572] Bug: merge "RECURSIVE"-keyword --- application/libraries/SearchBarLib.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index 91abeaebb..d563a9c79 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -675,6 +675,12 @@ class SearchBarLib SELECT " . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank / " . $count . " AS rank FROM " . $current_select; } + if ($or_with[0] === "RECURSIVE") { + if ($sqlWith[0] !== "RECURSIVE") + array_unshift($sqlWith, "RECURSIVE"); + array_shift($or_with); + } + $sqlWith = array_merge($sqlWith, $or_with); $sql_select = array_merge($sql_select, $or_select); $id_offset += count($or_with); From d0cf86585a174ef777accb102d71f0fcb6668169 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Tue, 5 Nov 2024 11:42:18 +0100 Subject: [PATCH 014/572] Visual update for search icons --- public/css/components/searchbar.css | 74 +++++++++++++------ public/js/components/searchbar/result/cms.js | 2 +- public/js/components/searchbar/result/dms.js | 2 +- .../components/searchbar/result/employee.js | 2 +- .../searchbar/result/mergedperson.js | 2 +- .../searchbar/result/organisationunit.js | 2 +- .../js/components/searchbar/result/person.js | 2 +- .../components/searchbar/result/prestudent.js | 2 +- public/js/components/searchbar/result/room.js | 2 +- .../js/components/searchbar/result/student.js | 2 +- .../searchbar/result/template/frame.js | 5 +- 11 files changed, 64 insertions(+), 33 deletions(-) diff --git a/public/css/components/searchbar.css b/public/css/components/searchbar.css index 61032d227..37eeaef83 100644 --- a/public/css/components/searchbar.css +++ b/public/css/components/searchbar.css @@ -97,37 +97,69 @@ /* new variant with template/frame */ .searchbar-result { - border-bottom: 1px solid lightgrey; - margin-bottom: 1rem; - padding-bottom: 1rem; + 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; + 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%; + 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; +} +.searchbar-square-image > div { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + color: rgba(var(--bs-white-rgb),1); + background-color: rgba(var(--bs-primary-rgb),var(--bs-bg-opacity)); +} +.searchbar-square-image.rounded-circle > img, +.searchbar-square-image.rounded-circle > div { + border-radius: 50%; } -.searchbar-square-image > * { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; +.searchbar-rounded-image { + display: block; + width: 100%; } -.searchbar-square-image img { - object-fit: cover; +.searchbar-rounded-image > img { + width: 100%; +} +.searchbar-rounded-image > div { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + color: rgba(var(--bs-white-rgb),1); + background-color: rgba(var(--bs-primary-rgb),var(--bs-bg-opacity)); + height: calc((100px - .75em) * 1.3); +} +.searchbar-rounded-image > img, +.searchbar-rounded-image > div { + border-radius: .25rem; } .no-margin-paragraphs p { - margin: 0; + margin: 0; } \ No newline at end of file diff --git a/public/js/components/searchbar/result/cms.js b/public/js/components/searchbar/result/cms.js index af2d6b0bb..5befdb413 100644 --- a/public/js/components/searchbar/result/cms.js +++ b/public/js/components/searchbar/result/cms.js @@ -69,7 +69,7 @@ export default { :res="res" :actions="actions" :title="res.title" - image-fallback="fas fa-newspaper fa-4x p-4 text-white bg-primary" + image-fallback="fas fa-newspaper fa-4x" @actionexecuted="$emit('actionexecuted')" > diff --git a/public/js/components/searchbar/result/dms.js b/public/js/components/searchbar/result/dms.js index 74fc9440b..ad770f1c6 100644 --- a/public/js/components/searchbar/result/dms.js +++ b/public/js/components/searchbar/result/dms.js @@ -44,7 +44,7 @@ export default { :res="res" :actions="actions" :title="res.name" - :image-fallback="'fas fa-' + icon + ' fa-4x p-5 text-white bg-primary'" + :image-fallback="'fas fa-' + icon + ' fa-4x'" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/employee.js b/public/js/components/searchbar/result/employee.js index 1c4da075a..2fa07bf5a 100644 --- a/public/js/components/searchbar/result/employee.js +++ b/public/js/components/searchbar/result/employee.js @@ -16,7 +16,7 @@ export default { :actions="actions" :title="res.name" :image="res.photo_url" - image-fallback="fas fa-user-circle fa-7x" + image-fallback="fas fa-user fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js index 8f12540be..cfb7fe5a6 100644 --- a/public/js/components/searchbar/result/mergedperson.js +++ b/public/js/components/searchbar/result/mergedperson.js @@ -48,7 +48,7 @@ export default { :actions="actions" :title="person.name" :image="this.person.photo_url" - image-fallback="fas fa-user-circle fa-7x" + image-fallback="fas fa-user fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/organisationunit.js b/public/js/components/searchbar/result/organisationunit.js index f9b6870e0..129479277 100644 --- a/public/js/components/searchbar/result/organisationunit.js +++ b/public/js/components/searchbar/result/organisationunit.js @@ -22,7 +22,7 @@ export default { :res="res" :actions="actions" :title="res.name" - image-fallback="fas fa-sitemap fa-4x p-4 text-white bg-primary" + image-fallback="fas fa-sitemap fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/person.js b/public/js/components/searchbar/result/person.js index daa968e72..bc4e75ee8 100644 --- a/public/js/components/searchbar/result/person.js +++ b/public/js/components/searchbar/result/person.js @@ -21,7 +21,7 @@ export default { :actions="actions" :title="res.name" :image="res.photo_url" - image-fallback="fas fa-user-circle fa-7x" + image-fallback="fas fa-user fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/prestudent.js b/public/js/components/searchbar/result/prestudent.js index 4a11ac941..f3df3e8ca 100644 --- a/public/js/components/searchbar/result/prestudent.js +++ b/public/js/components/searchbar/result/prestudent.js @@ -21,7 +21,7 @@ export default { :actions="actions" :title="res.name + ' (' + res.status + ' ' + res.stg_kuerzel + ')'" :image="res.photo_url" - image-fallback="fas fa-user-circle fa-7x" + image-fallback="fas fa-user fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/room.js b/public/js/components/searchbar/result/room.js index 6da1e970a..23ce6b463 100644 --- a/public/js/components/searchbar/result/room.js +++ b/public/js/components/searchbar/result/room.js @@ -33,7 +33,7 @@ export default { :res="res" :actions="actions" :title="res.ort_kurzbz" - image-fallback="fas fa-door-open fa-4x p-4 text-white bg-primary" + image-fallback="fas fa-door-open fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/student.js b/public/js/components/searchbar/result/student.js index 3eb401d97..bb3512e3a 100644 --- a/public/js/components/searchbar/result/student.js +++ b/public/js/components/searchbar/result/student.js @@ -21,7 +21,7 @@ export default { :actions="actions" :title="res.name" :image="res.photo_url" - image-fallback="fas fa-user-circle fa-7x" + image-fallback="fas fa-user fa-4x" @actionexecuted="$emit('actionexecuted')" >
diff --git a/public/js/components/searchbar/result/template/frame.js b/public/js/components/searchbar/result/template/frame.js index 87078a166..336c2d9f9 100644 --- a/public/js/components/searchbar/result/template/frame.js +++ b/public/js/components/searchbar/result/template/frame.js @@ -22,14 +22,13 @@ export default { :res="res" :action="actions.defaultaction" @actionexecuted="$emit('actionexecuted')" - class="searchbar-square-image" + class="searchbar-rounded-image" > -
+
From e3119ee48ce91d3a6e2785d234582d8c0e9479d5 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Tue, 5 Nov 2024 15:35:00 +0100 Subject: [PATCH 015/572] cleanup --- application/controllers/api/frontend/v1/Searchbar.php | 1 - application/libraries/SearchBarLib.php | 5 ----- 2 files changed, 6 deletions(-) diff --git a/application/controllers/api/frontend/v1/Searchbar.php b/application/controllers/api/frontend/v1/Searchbar.php index 2d5b06063..d630d3afd 100644 --- a/application/controllers/api/frontend/v1/Searchbar.php +++ b/application/controllers/api/frontend/v1/Searchbar.php @@ -50,7 +50,6 @@ 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 diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index d563a9c79..630f31ecf 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -32,11 +32,6 @@ class SearchBarLib const ERROR_WRONG_TYPES = 'ERR004'; const ERROR_NOT_AUTH = 'ERR005'; - // List of allowed types of search - const ALLOWED_TYPES = ['mitarbeiter', 'mitarbeiter_ohne_zuordnung', 'organisationunit', 'raum', 'person', 'student', 'prestudent', 'document', 'cms']; - - const PHOTO_IMG_URL = '/cis/public/bild.php?src=person&person_id='; - private $_ci; // Code igniter instance private $_searchfunction_priorities = []; From 8c689418f7fffe9c54d77aeb5406c1fe55576570 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Thu, 7 Nov 2024 09:57:26 +0100 Subject: [PATCH 016/572] Bugfixes --- application/libraries/SearchBarLib.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index 630f31ecf..b9c71b16b 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -68,7 +68,6 @@ 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) @@ -498,9 +497,8 @@ class SearchBarLib SELECT " . $this->_formatPrimarykeys($table_config['primarykey'], $table_config['table']) . " FROM " . $table_config['table'] . " - " . $this->_makeJoin($field_config['join']) . " + " . $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); } @@ -508,10 +506,10 @@ class SearchBarLib $or_select[] = " SELECT - " . $table_config['table']($table_config['primarykey']) . ", + " . $this->_formatPrimarykeys($table_config['primarykey'], $table_config['table']) . ", 1.0 AS rank FROM " . $table_config['table'] . " - WHERE prestudent_id NOT IN (" . implode(" UNION ", $sql) . ")"; + WHERE " . $table_config['primarykey'] . " NOT IN (" . implode(" UNION ", $sql) . ")"; } else { $current_select = false; $count = 0; From f82ed6928aaae0a6d1a648a4ea5d572b2ee6f11b Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Thu, 7 Nov 2024 11:47:35 +0100 Subject: [PATCH 017/572] Phrasen --- application/libraries/SearchBarLib.php | 8 +- public/js/components/searchbar/result/cms.js | 2 +- public/js/components/searchbar/result/dms.js | 11 +- .../components/searchbar/result/employee.js | 12 +- .../searchbar/result/mergedperson.js | 26 +- .../searchbar/result/organisationunit.js | 17 +- .../js/components/searchbar/result/person.js | 4 +- .../components/searchbar/result/prestudent.js | 12 +- public/js/components/searchbar/result/room.js | 16 +- .../js/components/searchbar/result/student.js | 8 +- .../searchbar/result/template/action.js | 2 +- public/js/components/searchbar/searchbar.js | 18 +- system/phrasesupdate.php | 682 +++++++++++++++++- 13 files changed, 744 insertions(+), 74 deletions(-) diff --git a/application/libraries/SearchBarLib.php b/application/libraries/SearchBarLib.php index b9c71b16b..a14319bba 100644 --- a/application/libraries/SearchBarLib.php +++ b/application/libraries/SearchBarLib.php @@ -95,7 +95,7 @@ class SearchBarLib return error(array_map(function ($type) use ($p) { return $p->t('search', 'error_missing_config', [ 'type' => $type - ]); // TODO(chris): phrase + ]); }, $missing)); } $types = $tmp; @@ -288,7 +288,7 @@ class SearchBarLib 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']) @@ -298,7 +298,7 @@ class SearchBarLib $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']) @@ -336,7 +336,7 @@ class SearchBarLib 'type' => $name, 'searchfield' => $searchfield, 'field' => 'field' - ]); // TODO(chris): phrase + ]); } if (!isset($config['comparison']) || !is_string($config['comparison']) diff --git a/public/js/components/searchbar/result/cms.js b/public/js/components/searchbar/result/cms.js index 5befdb413..591c7e79c 100644 --- a/public/js/components/searchbar/result/cms.js +++ b/public/js/components/searchbar/result/cms.js @@ -75,7 +75,7 @@ export default {
- No Content + {{ $p.t('search/result_content_none') }}
` }; \ No newline at end of file diff --git a/public/js/components/searchbar/result/dms.js b/public/js/components/searchbar/result/dms.js index ad770f1c6..75b281541 100644 --- a/public/js/components/searchbar/result/dms.js +++ b/public/js/components/searchbar/result/dms.js @@ -9,9 +9,6 @@ export default { res: Object, actions: Object }, - inject: [ - 'query' - ], computed: { icon() { switch (this.res.mimetype) { @@ -49,25 +46,25 @@ export default { >
-
DMS ID
+
{{ $p.t('search/result_dms_id') }}
{{ res.dms_id }}
-
Version
+
{{ $p.t('search/result_version') }}
{{ res.version }}
-
Keywords
+
{{ $p.t('search/result_keywords') }}
{{ res.keywords }}
-
Description
+
{{ $p.t('global/beschreibung') }}
{{ res.description }}
diff --git a/public/js/components/searchbar/result/employee.js b/public/js/components/searchbar/result/employee.js index 2fa07bf5a..a86fc6cbf 100644 --- a/public/js/components/searchbar/result/employee.js +++ b/public/js/components/searchbar/result/employee.js @@ -21,25 +21,25 @@ export default { >
-
Standard-Kostenstelle
+
{{ $p.t('search/result_stdkst') }}
  • {{ stdkst }}
- keine + {{ $p.t('search/result_stdkst_none') }}
-
Organisations-Einheit
+
{{ $p.t('lehre/organisationseinheit') }}
  • {{ oe }}
- keine + {{ $p.t('search/result_oe_none') }}
-
EMails
+
{{ $p.t('search/result_emails') }}
-
Telefon
+
{{ $p.t('person/telefon') }}
{{ res.phone }} diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js index cfb7fe5a6..c0349c2e3 100644 --- a/public/js/components/searchbar/result/mergedperson.js +++ b/public/js/components/searchbar/result/mergedperson.js @@ -47,19 +47,19 @@ export default { :res="person" :actions="actions" :title="person.name" - :image="this.person.photo_url" + :image="person.photo_url" image-fallback="fas fa-user fa-4x" @actionexecuted="$emit('actionexecuted')" >
-
Person ID
+
{{ $p.t('person/person_id') }}
{{ person.person_id }}
-
Standard-Kostenstelle
+
{{ $p.t('search/result_stdkst') }}
- keine + {{ $p.t('search/result_stdkst_none') }}
-
Organisations-Einheit
+
{{ $p.t('lehre/organisationseinheit') }}
- keine + {{ $p.t('search/result_oe_none') }}
-
Studiengang
+
{{ $p.t('lehre/studiengang') }}
{{ student.bezeichnung }}
-
Prestudent ID
+
{{ $p.t('search/result_prestudent_id') }}
{{ student.prestudent_id }}
-
Student UID
+
{{ $p.t('search/result_student_uid') }}
{{ student.uid }}
-
Matrikelnummer
+
{{ $p.t('person/matrikelnummer') }}
{{ student.matrikelnr }}
diff --git a/public/js/components/searchbar/result/organisationunit.js b/public/js/components/searchbar/result/organisationunit.js index 129479277..fe5d6512a 100644 --- a/public/js/components/searchbar/result/organisationunit.js +++ b/public/js/components/searchbar/result/organisationunit.js @@ -9,13 +9,6 @@ export default { res: Object, actions: Object }, - computed: { - foto() { - if (this.res.foto) - return 'data:image/jpeg;base64,' + this.res.foto; - return null; - } - }, template: `
-
übergeordnete OrgEinheit
+
{{ $p.t('search/result_parent_oe') }}
{{ res.parentoe_name }}
-
Gruppen-EMail
+
{{ $p.t('search/result_group_emails') }}
-
Leiter
+
{{ $p.t('search/result_leader') }}
  • {{ leader.name }}
- N.N. + {{ $p.t('search/result_leader_none') }}
-
Mitarbeiter-Anzahl
+
{{ $p.t('search/result_number_of_employees') }}
{{ res.number_of_people }}
diff --git a/public/js/components/searchbar/result/person.js b/public/js/components/searchbar/result/person.js index bc4e75ee8..f6ce0a5f0 100644 --- a/public/js/components/searchbar/result/person.js +++ b/public/js/components/searchbar/result/person.js @@ -26,13 +26,13 @@ export default { >
-
Person ID
+
{{ $p.t('person/person_id') }}
{{ res.person_id }}
-
EMails
+
{{ $p.t('search/result_emails') }}
{{ email }} diff --git a/public/js/components/searchbar/result/prestudent.js b/public/js/components/searchbar/result/prestudent.js index f3df3e8ca..51b4ccd9b 100644 --- a/public/js/components/searchbar/result/prestudent.js +++ b/public/js/components/searchbar/result/prestudent.js @@ -26,13 +26,13 @@ export default { >
-
Person ID
+
{{ $p.t('person/person_id') }}
{{ res.person_id }}
-
Student UID
+
{{ $p.t('search/result_student_uid') }}
{{ res.uid }}
-
Matrikelnummer
+
{{ $p.t('person/matrikelnummer') }}
{{ res.matrikelnr }}
-
Prestudent ID
+
{{ $p.t('search/result_prestudent_id') }}
{{ res.prestudent_id }}
-
Studiengang
+
{{ $p.t('lehre/studiengang') }}
{{ res.bezeichnung }}
diff --git a/public/js/components/searchbar/result/room.js b/public/js/components/searchbar/result/room.js index 23ce6b463..46fce1c71 100644 --- a/public/js/components/searchbar/result/room.js +++ b/public/js/components/searchbar/result/room.js @@ -22,9 +22,9 @@ export default { if (this.res.street) address += (address ? ', ' : '') + this.res.street; if (this.res.floor) - address += (address ? ' / ' : '') + this.res.floor + ' Stockwerk'; + address += (address ? ' / ' : '') + this.$p.t('search/result_address_floor', this.res); - return address || 'N/A'; + return address || this.$p.t('search/result_address_none'); } }, template: ` @@ -38,33 +38,33 @@ export default { >
-
Standort
+
{{ $p.t('search/result_room_address') }}
{{ address }}
-
Sitzplätze
+
{{ $p.t('search/result_workplaces') }}
-
Gebäude
+
{{ $p.t('search/result_building') }}
{{ res.building }}
-
Zusatz Informationen
+
{{ $p.t('search/result_equipment') }}
diff --git a/public/js/components/searchbar/result/student.js b/public/js/components/searchbar/result/student.js index bb3512e3a..e85716a94 100644 --- a/public/js/components/searchbar/result/student.js +++ b/public/js/components/searchbar/result/student.js @@ -26,25 +26,25 @@ export default { >
-
Student UID
+
{{ $p.t('search/result_student_uid') }}
{{ res.uid }}
-
Person ID
+
{{ $p.t('person/person_id') }}
{{ res.person_id }}
-
Matrikelnummer
+
{{ $p.t('person/matrikelnummer') }}
{{ res.matrikelnr }}
-
EMails
+
{{ $p.t('search/result_emails') }}
{{ email }} diff --git a/public/js/components/searchbar/result/template/action.js b/public/js/components/searchbar/result/template/action.js index a7654df59..f41d9925f 100644 --- a/public/js/components/searchbar/result/template/action.js +++ b/public/js/components/searchbar/result/template/action.js @@ -23,6 +23,6 @@ export default { }, template: ` - Action + {{ $p.t('search/action_default_label') }} ` }; \ No newline at end of file diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 7589cfbef..034cf842a 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -9,8 +9,6 @@ import ResultDms from "./result/dms.js"; import ResultMergedperson from "./result/mergedperson.js"; import ResultMergedstudent from "./result/mergedstudent.js"; -// TODO(chris): arrays in results - export default { components: { ResultPerson, @@ -108,7 +106,7 @@ export default { .searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal }) .then(response => { if (!response.data) { - this.error = 'Bei der Suche ist ein Fehler aufgetreten.'; + this.error = this.$p.t('search/error_general'); } else { let res = response.data.map(el => ({...el, ...JSON.parse(el.data)})); this.lastQuery = response.meta.searchstring; @@ -167,7 +165,7 @@ export default { return this.callsearchapi(); } - this.error = 'Bei der Suche ist ein Fehler aufgetreten.' + ' ' + error.message; + this.error = this.$p.t('search/error_general', error); this.searching = false; this.retry = 0; }); @@ -230,8 +228,8 @@ export default { v-model="searchsettings.searchstr" class="form-control" type="search" - placeholder="Suche..." - aria-label="Search" + :placeholder="$p.t('search/input_search_label')" + :aria-label="$p.t('search/input_search_label')" > @@ -254,7 +254,7 @@ export default {
{{ error }}
-
Es wurden keine Ergebnisse gefunden.
+
{{ $p.t('search/error_no_results') }}
@@ -302,7 +302,7 @@ export default { class="btn btn-primary" type="button" > - Übernehmen + {{ $p.t('search/button_applyfilter_label') }}
` diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index d9a72eaac..76059451d 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -31165,8 +31165,688 @@ array( 'insertvon' => 'system' ) ) + ), + //**************************** CORE/search + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'input_search_label', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Suche...', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Search...', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'button_filter_label', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Filter', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Filter', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'button_applyfilter_label', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Übernehmen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Apply', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'action_default_label', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Standard Aktion', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Default Action', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_student_uid', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Student UID', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Student UID', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_prestudent_id', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Prestudent ID', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Prestudent ID', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_emails', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Emails', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Emails', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_group_emails', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Gruppen-Email', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Group-Email', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_employee', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'MitarbeiterIn', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Employee', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_stdkst', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Standard-Kostenstelle', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Standard cost center', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_stdkst_none', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'keine', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'none', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_parent_oe', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Übergeordnete OE', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Parent OU', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_oe_none', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'keine', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'none', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_room_address', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Standort', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Site', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_address_floor', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '{floor} Stockwerk', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => '{floor} floor', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_address_none', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'N/A', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'N/A', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_workplaces', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sitzplätze', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Seats', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_workplaces_pc', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '{max_person}, davon {workplaces} PC-Plätze', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => '{max_person}, of which {workplaces} PC-Workstations', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_workplaces_none', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'N/A', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'N/A', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_building', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Gebäude', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Building', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_equipment', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Zusatz Informationen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Additional information', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_leader', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Leiter', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Leader', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_leader_none', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'N.N.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'N/A', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_number_of_employees', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Mitarbeiter-Anzahl', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Number of employees', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_dms_id', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'DMS ID', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'DMS ID', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_version', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Version', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Version', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_keywords', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Schlagwörter', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Keywords', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'result_content_none', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Kein Inhalt', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No Content', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'error_no_results', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Es wurden keine Ergebnisse gefunden.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No results were found.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'error_unknown_type', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Unbekannter Ergebnistyp: '{type}'.", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Unknown resulttype: '{type}'.", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'error_general', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bei der Suche ist ein Fehler aufgetreten. {message}', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'An error occurred while searching. {message}', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'error_missing_config', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Such-Konfiguration $config["{type}"] nicht gefunden.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Search config $config["{type}"] not found.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'error_invalid_config', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Such-Konfiguration für $config["{type}"] ist ungültig: Feld "{field}" fehlt, ist leer oder hat einen ungültigen Typ.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Search config for $config["{type}"] is invalid: field {field} is missing, empty or has an invalid type.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'search', + 'phrase' => 'error_invalid_config_searchfield', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Such-Konfiguration $config["{type}"]["searchfields"]["{searchfield}"] ist ungültig: Feld "{field}" fehlt oder ist unglültig.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Search config $config["{type}"]["searchfields"]["{searchfield}"] is invalid: field {field} is missing or invalid.', + 'description' => '', + 'insertvon' => 'system' + ) + ) ) - ); From 72c992569c5c79a49f7b1c8325db494533f612d4 Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Thu, 7 Nov 2024 13:33:51 +0100 Subject: [PATCH 018/572] Improvements Mergedperson Component --- .../searchbar/result/mergedperson.js | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js index c0349c2e3..e6fc5833b 100644 --- a/public/js/components/searchbar/result/mergedperson.js +++ b/public/js/components/searchbar/result/mergedperson.js @@ -12,33 +12,38 @@ export default { 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(); + // Cummulate all emails + const email = this.res.list.reduce((a, c) => [...a, ...(Array.isArray(c.email) ? c.email : [c.email])], []); + + // Use person entry if available (with cummulated emails) + const person = this.res.list.find(item => item.type == 'person'); + if (person) + return {...person, email}; + + // Those properties should be the same in all entries + const { person_id, name } = this.res.list[0]; + // Get first photo (prefer student photo if available) + const photo_url = ((this.students ? this.students.find(el => el.photo_url) : null) || this.employee)?.photo_url; - // TODO(chris): first one might have not one of these but a later one - const { person_id, name, photo_url, email } = this.res.list[0]; return { person_id, name, photo_url, email }; }, employee() { - const ma = this.res.list.filter(item => [ + return this.res.list.find(item => [ 'employee', 'unassigned_employee' - ].includes(item.type)); - return ma.length ? ma.pop() : null; + ].includes(item.type)) || null; }, students() { const students = this.res.list.filter(item => item.type == 'prestudent'); return students.length ? students : null; }, emails() { - if (Array.isArray(this.person.email)) - return new Set(this.person.email); - return [this.person.email]; + // Remove duplicates + return new Set(this.person.email); + }, + telurl() { + return 'tel:' + this.employee?.phone; } }, template: ` From 2ed5df8c1c18f644bac7de3ff0aad41c9103f7dc Mon Sep 17 00:00:00 2001 From: cgfhtw Date: Thu, 7 Nov 2024 17:01:51 +0100 Subject: [PATCH 019/572] Orgform of Stg in searchresults --- application/config/search.php | 13 ++++++++++++- .../js/components/searchbar/result/mergedperson.js | 2 +- public/js/components/searchbar/result/prestudent.js | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/application/config/search.php b/application/config/search.php index 8c601e046..3a2c27a18 100644 --- a/application/config/search.php +++ b/application/config/search.php @@ -309,7 +309,18 @@ $config['prestudent'] = [ FROM public.tbl_status WHERE status_kurzbz = public.get_rolle_prestudent(ps.prestudent_id, NULL) LIMIT 1 - ) as status" + ) AS status", + "COALESCE( + ( + SELECT COALESCE(plan.orgform_kurzbz, pss.orgform_kurzbz) + FROM public.tbl_prestudentstatus pss + LEFT JOIN lehre.tbl_studienplan plan USING (studienplan_id) + WHERE pss.prestudent_id=ps.prestudent_id + ORDER BY pss.datum DESC, pss.insertamum DESC, pss.ext_id DESC + LIMIT 1 + ), + sg.orgform_kurzbz + ) AS orgform" ], 'resultjoin' => " LEFT JOIN public.tbl_prestudent ps USING (prestudent_id) diff --git a/public/js/components/searchbar/result/mergedperson.js b/public/js/components/searchbar/result/mergedperson.js index e6fc5833b..be9d1e735 100644 --- a/public/js/components/searchbar/result/mergedperson.js +++ b/public/js/components/searchbar/result/mergedperson.js @@ -139,7 +139,7 @@ export default {
{{ $p.t('lehre/studiengang') }}
- {{ student.bezeichnung }} + {{ student.bezeichnung }} {{ student.orgform ? '(' + student.orgform + ')' : '' }}
diff --git a/public/js/components/searchbar/result/prestudent.js b/public/js/components/searchbar/result/prestudent.js index 51b4ccd9b..c33877bf0 100644 --- a/public/js/components/searchbar/result/prestudent.js +++ b/public/js/components/searchbar/result/prestudent.js @@ -60,7 +60,7 @@ export default {
{{ $p.t('lehre/studiengang') }}
- {{ res.bezeichnung }} + {{ res.bezeichnung }} {{ res.orgform ? '(' + res.orgform + ')' : '' }}
From 7fbff20dd97d66a15cd0242214c96616530fe924 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 4 Feb 2025 14:49:41 +0100 Subject: [PATCH 020/572] new Tab Gruppen --- .../api/frontend/v1/stv/Config.php | 4 + .../api/frontend/v1/stv/Favorites.php | 2 +- .../api/frontend/v1/stv/Gruppen.php | 81 ++++++++ public/js/api/stv.js | 2 + public/js/api/stv/group.js | 8 + .../js/components/Stv/Studentenverwaltung.js | 1 + .../Studentenverwaltung/Details/Gruppen.js | 19 ++ .../Details/Gruppen/Gruppen.js | 182 ++++++++++++++++++ system/phrasesupdate.php | 83 ++++++++ 9 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 application/controllers/api/frontend/v1/stv/Gruppen.php create mode 100644 public/js/api/stv/group.js create mode 100644 public/js/components/Stv/Studentenverwaltung/Details/Gruppen.js create mode 100644 public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js diff --git a/application/controllers/api/frontend/v1/stv/Config.php b/application/controllers/api/frontend/v1/stv/Config.php index c28c49485..12cd77048 100644 --- a/application/controllers/api/frontend/v1/stv/Config.php +++ b/application/controllers/api/frontend/v1/stv/Config.php @@ -91,6 +91,10 @@ class Config extends FHCAPI_Controller 'title' => $this->p->t('stv', 'tab_resources'), 'component' => './Stv/Studentenverwaltung/Details/Betriebsmittel.js' ]; + $result['groups'] = [ + 'title' => $this->p->t('stv', 'tab_groups'), + 'component' => './Stv/Studentenverwaltung/Details/Gruppen.js' + ]; /* TODO(chris): Ausgeblendet für Testing $result['grades'] = [ 'title' => $this->p->t('stv', 'tab_grades'), diff --git a/application/controllers/api/frontend/v1/stv/Favorites.php b/application/controllers/api/frontend/v1/stv/Favorites.php index 8d7a6cd14..b8fe6f3d7 100644 --- a/application/controllers/api/frontend/v1/stv/Favorites.php +++ b/application/controllers/api/frontend/v1/stv/Favorites.php @@ -48,7 +48,7 @@ class Favorites extends FHCAPI_Controller if (!$data) $this->terminateWithSuccess(null); else - $this->terminateWithSuccess($data['stv_favorites']); + $this->terminateWithSuccess($data['stv_favorites'] ?? null); } public function set() diff --git a/application/controllers/api/frontend/v1/stv/Gruppen.php b/application/controllers/api/frontend/v1/stv/Gruppen.php new file mode 100644 index 000000000..39c5efe21 --- /dev/null +++ b/application/controllers/api/frontend/v1/stv/Gruppen.php @@ -0,0 +1,81 @@ + ['admin:r', 'assistenz:r'], + 'deleteGruppe' => ['admin:rw', 'assistenz:rw'], + ]); + + // Load Libraries + $this->load->library('VariableLib', ['uid' => getAuthUID()]); + + // Load language phrases + $this->loadPhrases([ + 'ui', 'gruppenmanagement' + ]); + + // Load models + $this->load->model('person/Benutzergruppe_model', 'BenutzergruppeModel'); + $this->load->model('organisation/Gruppe_model', 'GruppeModel'); + } + + public function getGruppen($student_uid) + { + $this->BenutzergruppeModel ->addSelect('gruppe_kurzbz'); + $this->BenutzergruppeModel ->addSelect('bezeichnung'); + $this->BenutzergruppeModel ->addSelect('generiert'); + $this->BenutzergruppeModel ->addSelect('uid'); + $this->BenutzergruppeModel ->addSelect('studiensemester_kurzbz'); + $this->BenutzergruppeModel ->addSelect('public.tbl_benutzergruppe.insertvon'); + $this->BenutzergruppeModel ->addJoin('public.tbl_gruppe', 'gruppe_kurzbz'); + $this->BenutzergruppeModel-> addOrder('bezeichnung', 'ASC'); + + $result = $this->BenutzergruppeModel->loadWhere( + array( + 'uid' => $student_uid + ) + ); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function deleteGruppe() + { + $student_uid = $this->input->post('id'); + $gruppe_kurzbz = $this->input->post('gruppe_kurzbz'); + + //Validate if automatic group generation + $result = $this->GruppeModel-> loadWhere( + array( + 'gruppe_kurzbz' => $gruppe_kurzbz + ) + ); + $data = $this->getDataOrTerminateWithError($result); + $generation = current($data); + + if($generation->generiert) + { + $this->terminateWithError($this->p->t('gruppenmanagement', 'error_deleteGeneratedGroups'), self::ERROR_TYPE_GENERAL); + } + + $result = $this->BenutzergruppeModel->delete( + array( + 'gruppe_kurzbz' => $gruppe_kurzbz, + 'uid' => $student_uid + ) + ); + + $data = $this->getDataOrTerminateWithError($result); + + return $this->terminateWithSuccess($data); + } +} diff --git a/public/js/api/stv.js b/public/js/api/stv.js index 14fcc6661..8b14beb14 100644 --- a/public/js/api/stv.js +++ b/public/js/api/stv.js @@ -2,12 +2,14 @@ import verband from './stv/verband.js'; import students from './stv/students.js'; import filter from './stv/filter.js'; import konto from './stv/konto.js'; +import group from './stv/group.js'; export default { verband, students, filter, konto, + group, configStudent() { return this.$fhcApi.get('api/frontend/v1/stv/config/student'); }, diff --git a/public/js/api/stv/group.js b/public/js/api/stv/group.js new file mode 100644 index 000000000..af6e6e122 --- /dev/null +++ b/public/js/api/stv/group.js @@ -0,0 +1,8 @@ +export default { + getGruppen(url, config, params) { + return this.$fhcApi.get('api/frontend/v1/stv/Gruppen/getGruppen/' + params.id); + }, + deleteGroup(params) { + return this.$fhcApi.post('api/frontend/v1/stv/Gruppen/deleteGruppe/', params); + } +} \ No newline at end of file diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index 8779e4bf1..eaa253c2c 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -57,6 +57,7 @@ export default { hasPermissionToSkipStatusCheck: this.permissions['student/keine_studstatuspruefung'], hasPermissionRtAufsicht: this.permissions['lehre/reihungstestAufsicht'], lists: this.lists, + currentSemester: Vue.computed(() => this.studiensemesterKurzbz), defaultSemester: this.defaultSemester, $reloadList: () => { this.$refs.stvList.reload(); diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Gruppen.js b/public/js/components/Stv/Studentenverwaltung/Details/Gruppen.js new file mode 100644 index 000000000..1c7c47d6b --- /dev/null +++ b/public/js/components/Stv/Studentenverwaltung/Details/Gruppen.js @@ -0,0 +1,19 @@ +import GruppenList from './Gruppen/Gruppen.js'; + +export default { + components: { + GruppenList + }, + props: { + modelValue: Object + }, + methods: { + reload() { + this.$refs.gruppen.$refs.table.reloadTable(); + } + }, + template: ` +
+ +
` +}; \ No newline at end of file diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js b/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js new file mode 100644 index 000000000..6a657a793 --- /dev/null +++ b/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js @@ -0,0 +1,182 @@ +import {CoreFilterCmpt} from "../../../../filter/Filter.js"; + +export default { + components: { + CoreFilterCmpt, + }, + inject: { + currentSemester: { + from: 'currentSemester', + }, + }, + props: { + student: Object + }, + data() { + return { + tabulatorOptions: { + ajaxURL: 'dummy', + ajaxRequestFunc: this.$fhcApi.factory.stv.group.getGruppen, + ajaxParams: () => { + return { + id: this.student.uid + }; + }, + ajaxResponse: (url, params, response) => response.data, + initialFilter: [ + {field: "uid", type: "=", value: this.student.uid}, + [ + {field: "studiensemester_kurzbz", type: "=", value: this.currentSemester}, + {field: "insertvon", type: "=", value: "mlists_generate"} + ] + ], + columns: [ + {title: "Gruppe", field: "gruppe_kurzbz"}, + {title: "Bezeichnung", field: "bezeichnung"}, + {title: "Semester", field: "studiensemester_kurzbz"}, + { + title: "automatisch generiert", + field: "generiert", + formatter: "tickCross", + hozAlign: "center", + formatterParams: { + tickElement: '', + crossElement: '' + } + }, + {title: "UID", field: "uid"}, + {title: "InsertVon", field: "insertvon", visible: false}, + { + title: 'Aktionen', field: 'actions', + minWidth: 150, // Ensures Action-buttons will be always fully displayed + formatter: (cell, formatterParams, onRendered) => { + const container = document.createElement('div'); + container.className = "d-flex gap-2"; + + const data = cell.getData(); + + const button = document.createElement('button'); + button.className = 'btn btn-outline-secondary btn-action'; + button.innerHTML = ''; + button.title = this.$p.t('ui', 'loeschen'); + button.addEventListener('click', () => + this.actionDeleteGroup(data.gruppe_kurzbz) + ); + if (data.generiert) + button.disabled = true; + container.append(button); + + return container; + }, + frozen: true + }, + ], + layout: 'fitDataFill', + height: 'auto', + selectable: true, + index: 'group_id', + persistenceID: 'stv-details-gruppe' + }, + tabulatorEvents: [ + { + event: 'tableBuilt', + handler: async () => { + + await this.$p.loadCategory(['global', 'person', 'stv', 'ui', 'gruppenmanagement']); + + let cm = this.$refs.table.tabulator.columnManager; + + cm.getColumnByField('gruppe_kurzbz').component.updateDefinition({ + title: this.$p.t('gruppenmanagement', 'gruppe') + }); + + cm.getColumnByField('bezeichnung').component.updateDefinition({ + title: this.$p.t('ui', 'bezeichnung') + }); + + cm.getColumnByField('generiert').component.updateDefinition({ + title: this.$p.t('gruppenmanagement', 'automatisch_generiert') + }); + + cm.getColumnByField('uid').component.updateDefinition({ + title: this.$p.t('ui', 'student_uid') + }); + + //Interference with Filter if not commented out + /* + cm.getColumnByField('studiensemester_kurzbz').component.updateDefinition({ + title: this.$p.t('lehre', 'studiensemester') + });*/ + + } + } + ], + } + }, + methods: { + actionDeleteGroup(gruppe_kurzbz) { + this.$fhcAlert + .confirmDelete() + .then(result => result + ? gruppe_kurzbz + : Promise.reject({handled: true})) + .then(this.deleteGroup) + .catch(this.$fhcAlert.handleSystemError); + + }, + deleteGroup(gruppe_kurzbz) { + const group_id = { + id: this.student.uid, + gruppe_kurzbz: gruppe_kurzbz + }; + + return this.$fhcApi.factory.stv.group.deleteGroup(group_id) + .then(response => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete')); + }).catch(this.$fhcAlert.handleSystemError) + .finally(() => { + window.scrollTo(0, 0); + this.reload(); + }); + }, + reload() { + this.$refs.table.reloadTable(); + }, + }, + watch: { + currentSemester(newVal) { + if (newVal) { + + this.$refs.table.tabulator.clearFilter(); // Clear old filters + + this.$refs.table.tabulator.setFilter([ + {field: "uid", type: "=", value: this.student.uid}, + [ + {field: "studiensemester_kurzbz", type: "=", value: newVal}, + {field: "insertvon", type: "=", value: "mlists_generate"} + ] + ]); + + + } + }, + student() { + this.$refs.table.reloadTable(); + } + }, + template: ` +
+
{{$p.t('stv', 'tab_groups')}}
+ + + +
+ ` +} \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 746c24da3..160db2853 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -37297,7 +37297,90 @@ array( 'insertvon' => 'system' ) ) + ), + //////////// FHC4 Phrases Gruppen Start //////////// + array( + 'app' => 'core', + 'category' => 'stv', + 'phrase' => 'tab_groups', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Gruppen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Groups', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'gruppenmanagement', + 'phrase' => 'gruppe', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Gruppe', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Group', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'gruppenmanagement', + 'phrase' => 'automatisch_generiert', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'automatisch generiert', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'automatically generated', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'gruppenmanagement', + 'phrase' => 'error_deleteGeneratedGroups', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Automatisch generierte Gruppenzuordnungen können nicht gelöscht werden.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Automatically generated group assignments cannot be deleted.', + 'description' => '', + 'insertvon' => 'system' + ) + ) ) + //////////// FHC4 Phrases Gruppen End //////////// + ); From 6eb9c2ac223e34fe6bd5e89310615e3822e58bf2 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 4 Feb 2025 14:57:35 +0100 Subject: [PATCH 021/572] update filtering --- application/controllers/api/frontend/v1/stv/Gruppen.php | 1 - .../Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/application/controllers/api/frontend/v1/stv/Gruppen.php b/application/controllers/api/frontend/v1/stv/Gruppen.php index 39c5efe21..c30816f2a 100644 --- a/application/controllers/api/frontend/v1/stv/Gruppen.php +++ b/application/controllers/api/frontend/v1/stv/Gruppen.php @@ -33,7 +33,6 @@ class Gruppen extends FHCAPI_Controller $this->BenutzergruppeModel ->addSelect('generiert'); $this->BenutzergruppeModel ->addSelect('uid'); $this->BenutzergruppeModel ->addSelect('studiensemester_kurzbz'); - $this->BenutzergruppeModel ->addSelect('public.tbl_benutzergruppe.insertvon'); $this->BenutzergruppeModel ->addJoin('public.tbl_gruppe', 'gruppe_kurzbz'); $this->BenutzergruppeModel-> addOrder('bezeichnung', 'ASC'); diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js b/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js index 6a657a793..63a9b24e1 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Gruppen/Gruppen.js @@ -27,7 +27,7 @@ export default { {field: "uid", type: "=", value: this.student.uid}, [ {field: "studiensemester_kurzbz", type: "=", value: this.currentSemester}, - {field: "insertvon", type: "=", value: "mlists_generate"} + {field: "studiensemester_kurzbz", type: "=", value: null} ] ], columns: [ @@ -45,7 +45,6 @@ export default { } }, {title: "UID", field: "uid"}, - {title: "InsertVon", field: "insertvon", visible: false}, { title: 'Aktionen', field: 'actions', minWidth: 150, // Ensures Action-buttons will be always fully displayed @@ -153,7 +152,7 @@ export default { {field: "uid", type: "=", value: this.student.uid}, [ {field: "studiensemester_kurzbz", type: "=", value: newVal}, - {field: "insertvon", type: "=", value: "mlists_generate"} + {field: "studiensemester_kurzbz", type: "=", value: null} ] ]); From 018220594a82590cffbcbc612f0f767a314387d7 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Thu, 6 Feb 2025 08:34:34 +0100 Subject: [PATCH 022/572] prepare basic framework --- .../api/frontend/v1/messages/Messages.php | 45 +++++++ .../api/frontend/v1/stv/Config.php | 4 + .../api/frontend/v1/stv/Favorites.php | 2 +- application/models/system/Message_model.php | 9 +- public/js/api/fhcapifactory.js | 4 +- public/js/api/messages.js | 5 + public/js/api/messages/person.js | 6 + .../components/Messages/Details/NewMessage.js | 13 ++ .../Messages/Details/TableMessages.js | 117 ++++++++++++++++++ public/js/components/Messages/Messages.js | 48 +++++++ .../Studentenverwaltung/Details/Messages.js | 25 ++++ system/phrasesupdate.php | 25 +++- 12 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 application/controllers/api/frontend/v1/messages/Messages.php create mode 100644 public/js/api/messages.js create mode 100644 public/js/api/messages/person.js create mode 100644 public/js/components/Messages/Details/NewMessage.js create mode 100644 public/js/components/Messages/Details/TableMessages.js create mode 100644 public/js/components/Messages/Messages.js create mode 100644 public/js/components/Stv/Studentenverwaltung/Details/Messages.js diff --git a/application/controllers/api/frontend/v1/messages/Messages.php b/application/controllers/api/frontend/v1/messages/Messages.php new file mode 100644 index 000000000..f81e85e9a --- /dev/null +++ b/application/controllers/api/frontend/v1/messages/Messages.php @@ -0,0 +1,45 @@ + ['admin:r', 'assistenz:r'], + ]); + + //Load Models + $this->load->model('system/Message_model', 'MessageModel'); + + // Additional Permission Checks + //TODO(manu) check permissions + + // Load Libraries + $this->load->library('VariableLib', ['uid' => getAuthUID()]); + $this->load->library('form_validation'); + + // Load language phrases + $this->loadPhrases([ + 'ui' + ]); + } + + public function getMessages($id, $type_id) + { + //$this->terminateWithError("in backend " . $type_id . ": " . $id, self::ERROR_TYPE_GENERAL); + + if ($type_id != "person_id") + { + $this->terminateWithError("logic for type_id " . $type_id . " not defined yet", self::ERROR_TYPE_GENERAL); + } + + $result = $this->MessageModel->getMessagesOfPerson($id); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } +} \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/stv/Config.php b/application/controllers/api/frontend/v1/stv/Config.php index c28c49485..9dec3015f 100644 --- a/application/controllers/api/frontend/v1/stv/Config.php +++ b/application/controllers/api/frontend/v1/stv/Config.php @@ -91,6 +91,10 @@ class Config extends FHCAPI_Controller 'title' => $this->p->t('stv', 'tab_resources'), 'component' => './Stv/Studentenverwaltung/Details/Betriebsmittel.js' ]; + $result['messages'] = [ + 'title' => $this->p->t('stv', 'tab_messages'), + 'component' => './Stv/Studentenverwaltung/Details/Messages.js' + ]; /* TODO(chris): Ausgeblendet für Testing $result['grades'] = [ 'title' => $this->p->t('stv', 'tab_grades'), diff --git a/application/controllers/api/frontend/v1/stv/Favorites.php b/application/controllers/api/frontend/v1/stv/Favorites.php index 8d7a6cd14..b8fe6f3d7 100644 --- a/application/controllers/api/frontend/v1/stv/Favorites.php +++ b/application/controllers/api/frontend/v1/stv/Favorites.php @@ -48,7 +48,7 @@ class Favorites extends FHCAPI_Controller if (!$data) $this->terminateWithSuccess(null); else - $this->terminateWithSuccess($data['stv_favorites']); + $this->terminateWithSuccess($data['stv_favorites'] ?? null); } public function set() diff --git a/application/models/system/Message_model.php b/application/models/system/Message_model.php index d9f8585ed..6288f54f3 100644 --- a/application/models/system/Message_model.php +++ b/application/models/system/Message_model.php @@ -85,7 +85,7 @@ class Message_model extends DB_Model */ public function getMessagesOfPerson($person_id, $status = null) { - $sql = 'SELECT m.message_id, + $sql = "SELECT m.message_id, m.person_id, m.subject, m.body, @@ -109,7 +109,10 @@ class Message_model extends DB_Model re.vornamen AS revornamen, s.status, s.statusinfo, - s.insertamum AS statusamum + s.insertamum AS statusamum, + CONCAT(se.titelpre, ' ', se.vorname, ' ', se.nachname, ' ', se.titelpost) as sender, + CONCAT(re.titelpre, ' ', re.vorname, ' ', re.nachname, ' ', re.titelpost ) as recipient, + TO_CHAR(s.insertamum::timestamp, 'DD.MM.YYYY HH24:MI') AS format_insertamum FROM public.tbl_msg_message m JOIN public.tbl_msg_recipient r ON m.message_id = r.message_id JOIN public.tbl_person se ON (m.person_id = se.person_id) @@ -122,7 +125,7 @@ class Message_model extends DB_Model ) s ON (m.message_id = s.message_id AND re.person_id = s.person_id) WHERE se.person_id = ? OR re.person_id = ? - '; + "; if (is_numeric($status)) { diff --git a/public/js/api/fhcapifactory.js b/public/js/api/fhcapifactory.js index c4106c3f6..7bc510b6e 100644 --- a/public/js/api/fhcapifactory.js +++ b/public/js/api/fhcapifactory.js @@ -33,6 +33,7 @@ import ort from "./ort.js"; import cms from "./cms.js"; import lehre from "./lehre.js"; import addons from "./addons.js"; +import messages from "./messages.js"; export default { search, @@ -52,5 +53,6 @@ export default { ort, cms, lehre, - addons + addons, + messages }; diff --git a/public/js/api/messages.js b/public/js/api/messages.js new file mode 100644 index 000000000..f1378c69d --- /dev/null +++ b/public/js/api/messages.js @@ -0,0 +1,5 @@ +import person from "./messages/person.js"; + +export default { + person +} \ No newline at end of file diff --git a/public/js/api/messages/person.js b/public/js/api/messages/person.js new file mode 100644 index 000000000..bfbdaf123 --- /dev/null +++ b/public/js/api/messages/person.js @@ -0,0 +1,6 @@ +export default { + getMessages(url, config, params){ + console.log("in api", params); + return this.$fhcApi.get('api/frontend/v1/messages/messages/getMessages/' + params.id + '/' + params.type); + }, +} \ No newline at end of file diff --git a/public/js/components/Messages/Details/NewMessage.js b/public/js/components/Messages/Details/NewMessage.js new file mode 100644 index 000000000..bf60842d2 --- /dev/null +++ b/public/js/components/Messages/Details/NewMessage.js @@ -0,0 +1,13 @@ +export default { + data(){ + return { + + } + }, + template: ` +
+

New Message

+
+ ` + +} \ No newline at end of file diff --git a/public/js/components/Messages/Details/TableMessages.js b/public/js/components/Messages/Details/TableMessages.js new file mode 100644 index 000000000..bc7e6a02a --- /dev/null +++ b/public/js/components/Messages/Details/TableMessages.js @@ -0,0 +1,117 @@ +import {CoreFilterCmpt} from "../../filter/Filter.js"; + +export default { + components: { + CoreFilterCmpt, + }, + props: { + endpoint: { + type: String, + required: true + }, + typeId: String, + id: { + type: [Number, String], + required: true + }, + }, + //TODO(Manu) endpoint macht Probleme + data(){ + return { + tabulatorOptions: { + ajaxURL: 'dummy', + ajaxRequestFunc: this.$fhcApi.factory.messages.person.getMessages, + ajaxParams: () => { + return { + id: this.id, + type: this.typeId + }; + }, + ajaxResponse: (url, params, response) => response.data, + columns: [ + {title: "subject", field: "subject"}, + {title: "body", field: "body", visible: false}, + {title: "message_id", field: "message_id", visible: false}, + {title: "datum", field: "format_insertamum"}, + {title: "sender", field: "sender"}, + {title: "recipient", field: "recipient"}, + {title: "sepersonid", field: "sepersonid"}, + {title: "repersonid", field: "repersonid"}, + {title: "status", field: "status"}, + { + title: 'Aktionen', field: 'actions', + width: 100, + formatter: (cell, formatterParams, onRendered) => { + let container = document.createElement('div'); + container.className = "d-flex gap-2"; + + let button = document.createElement('button'); + button.className = 'btn btn-outline-secondary btn-action'; + button.title = this.$p.t('ui', 'notiz_edit'); + button.innerHTML = ''; + button.addEventListener( + 'click', + (event) => + this.actionEditNotiz(cell.getData().notiz_id) + ); + container.append(button); + + button = document.createElement('button'); + button.className = 'btn btn-outline-secondary btn-action'; + button.title = this.$p.t('notiz', 'notiz_delete'); + button.innerHTML = ''; + button.addEventListener( + 'click', + () => + this.actionDeleteNotiz(cell.getData().notiz_id) + ); + container.append(button); + + return container; + }, + frozen: true + }], + layout: 'fitColumns', + layoutColumnsOnNewData: false, + height: '250', + selectableRangeMode: 'click', + selectable: true, + index: 'message_id', + persistenceID: 'core-message' + }, + } + }, +/* computed: { + statusText(){ + 0: this.$p.t('messsages', 'unread'), + 1: this.$p.t('messsages', 'read'), + 2: this.$p.t('messsages', 'archived'), + 0: this.$p.t('messsages', 'unread'), + 3: this.$p.t('person', 'deleted'), + } + },*/ + template: ` +
+

Table Messages

+

type_id: {{typeId}}

+

id: {{id}}

+

endpoint: {{endpoint}}

+ + + + + + +
+ ` + +} \ No newline at end of file diff --git a/public/js/components/Messages/Messages.js b/public/js/components/Messages/Messages.js new file mode 100644 index 000000000..6811a3aed --- /dev/null +++ b/public/js/components/Messages/Messages.js @@ -0,0 +1,48 @@ +import TableMessages from "./Details/TableMessages.js"; +import NewMessage from "./Details/NewMessage.js"; + +export default { + components: { + TableMessages, + NewMessage + }, + props: { + endpoint: { + type: String, + required: true + }, + typeId: String, + id: { + type: [Number, String], + required: true + }, + showNew: Boolean, + showTable: Boolean + }, + data() { + return {} + }, + template: ` +
+

endpoint: {{endpoint}}

+
+ + +
+ +
+ + + +
+ +
+ ` + +} \ No newline at end of file diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Messages.js b/public/js/components/Stv/Studentenverwaltung/Details/Messages.js new file mode 100644 index 000000000..7bd6d4b4d --- /dev/null +++ b/public/js/components/Stv/Studentenverwaltung/Details/Messages.js @@ -0,0 +1,25 @@ +import CoreMessages from "../../../Messages/Messages.js"; + +export default { + components: { + CoreMessages + }, + props: { + modelValue: Object + }, + template: ` +
+ + + + + +
+ ` +}; \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index cfaf85ec7..f25cc72ec 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -37297,7 +37297,30 @@ array( 'insertvon' => 'system' ) ) - ) + ), + /////////// FHC4 Phrases Messages START /////////// + array( + 'app' => 'core', + 'category' => 'stv', + 'phrase' => 'tab_messages', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nachrichten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Messages', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + /////////// FHC4 Phrases Messages END /////////// + ); From ae3072be924940441968f5d65a352245af043afc Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 11 Feb 2025 09:02:56 +0100 Subject: [PATCH 023/572] listTables with preview in two layouts --- .../api/frontend/v1/messages/Messages.php | 2 +- application/models/system/Message_model.php | 57 +++- public/css/Studentenverwaltung.css | 1 + public/css/components/Messages.css | 4 + public/js/api/messages/person.js | 1 - .../Messages/Details/TableMessages.js | 272 +++++++++++++++--- public/js/components/Messages/Messages.js | 15 +- .../Studentenverwaltung/Details/Messages.js | 3 +- system/phrasesupdate.php | 200 +++++++++++++ 9 files changed, 508 insertions(+), 47 deletions(-) create mode 100644 public/css/components/Messages.css diff --git a/application/controllers/api/frontend/v1/messages/Messages.php b/application/controllers/api/frontend/v1/messages/Messages.php index f81e85e9a..adf1d52e9 100644 --- a/application/controllers/api/frontend/v1/messages/Messages.php +++ b/application/controllers/api/frontend/v1/messages/Messages.php @@ -36,7 +36,7 @@ class Messages extends FHCAPI_Controller $this->terminateWithError("logic for type_id " . $type_id . " not defined yet", self::ERROR_TYPE_GENERAL); } - $result = $this->MessageModel->getMessagesOfPerson($id); + $result = $this->MessageModel->getMessagesForTable($id); $data = $this->getDataOrTerminateWithError($result); diff --git a/application/models/system/Message_model.php b/application/models/system/Message_model.php index 6288f54f3..1b201fc1b 100644 --- a/application/models/system/Message_model.php +++ b/application/models/system/Message_model.php @@ -109,10 +109,7 @@ class Message_model extends DB_Model re.vornamen AS revornamen, s.status, s.statusinfo, - s.insertamum AS statusamum, - CONCAT(se.titelpre, ' ', se.vorname, ' ', se.nachname, ' ', se.titelpost) as sender, - CONCAT(re.titelpre, ' ', re.vorname, ' ', re.nachname, ' ', re.titelpost ) as recipient, - TO_CHAR(s.insertamum::timestamp, 'DD.MM.YYYY HH24:MI') AS format_insertamum + s.insertamum AS statusamum FROM public.tbl_msg_message m JOIN public.tbl_msg_recipient r ON m.message_id = r.message_id JOIN public.tbl_person se ON (m.person_id = se.person_id) @@ -233,4 +230,56 @@ class Message_model extends DB_Model return $this->execQuery($query, $params); } + + /** + * Gets messages for a person for tableMessages. + * @param $person_id + * @param null $status message status. by default, latest status is returned + * @return array|null + */ + public function getMessagesForTable($person_id, $status = null) + { + $sql = " + SELECT + m.message_id AS message_id, + m.subject AS subject, + m.body AS body, + m.insertamum AS insertamum, + m.relationmessage_id AS relationmessage_id, + (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = m.person_id) as sender, + (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = r.person_id) as recipient, + m.person_id as sender_id, + r.person_id as recipient_id, + MAX(ss.status) as status, + MAX(ss.insertamum) as statusdatum + FROM public.tbl_msg_message m + JOIN public.tbl_msg_recipient r USING(message_id) + JOIN public.tbl_msg_status ss ON(r.message_id = ss.message_id AND ss.person_id = r.person_id) + WHERE m.person_id = ? + GROUP BY m.message_id, m.subject, m.body, m.insertamum, m.relationmessage_id, sender, recipient, sender_id, recipient_id + UNION ALL + SELECT + m.message_id AS message_id, + m.subject AS subject, + m.body AS body, + m.insertamum AS insertamum, + m.relationmessage_id AS relationmessage_id, + (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = m.person_id) as sender, + (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = r.person_id) as recipient, + m.person_id as sender_id, + r.person_id as recipient_id, + MAX(ss.status) as status, + MAX(ss.insertamum) as statusdatum + FROM public.tbl_msg_recipient r + JOIN public.tbl_msg_status ss USING(message_id, person_id) + JOIN public.tbl_msg_message m USING(message_id) + WHERE r.person_id = ? + GROUP BY m.message_id, m.subject, m.body, m.insertamum, m.relationmessage_id, sender, recipient, sender_id, recipient_id + ORDER BY insertamum + "; + + $parametersArray = array($person_id, $person_id); + + return $this->execQuery($sql, $parametersArray); + } } diff --git a/public/css/Studentenverwaltung.css b/public/css/Studentenverwaltung.css index f179c3667..cc2eff51d 100644 --- a/public/css/Studentenverwaltung.css +++ b/public/css/Studentenverwaltung.css @@ -4,6 +4,7 @@ @import './components/FilterComponent.css'; @import './components/Tabs.css'; @import './components/Notiz.css'; +@import './components/Messages.css'; html { font-size: .875em; diff --git a/public/css/components/Messages.css b/public/css/components/Messages.css new file mode 100644 index 000000000..1c004f6b0 --- /dev/null +++ b/public/css/components/Messages.css @@ -0,0 +1,4 @@ +.twoColumns { + height: 400px; + overflow-y: auto; +} diff --git a/public/js/api/messages/person.js b/public/js/api/messages/person.js index bfbdaf123..baf8bd39f 100644 --- a/public/js/api/messages/person.js +++ b/public/js/api/messages/person.js @@ -1,6 +1,5 @@ export default { getMessages(url, config, params){ - console.log("in api", params); return this.$fhcApi.get('api/frontend/v1/messages/messages/getMessages/' + params.id + '/' + params.type); }, } \ No newline at end of file diff --git a/public/js/components/Messages/Details/TableMessages.js b/public/js/components/Messages/Details/TableMessages.js index bc7e6a02a..c4fd4d4d7 100644 --- a/public/js/components/Messages/Details/TableMessages.js +++ b/public/js/components/Messages/Details/TableMessages.js @@ -1,8 +1,12 @@ import {CoreFilterCmpt} from "../../filter/Filter.js"; +import FormForm from '../../Form/Form.js'; +//import FormInput from '../../Form/Input.js'; export default { components: { CoreFilterCmpt, + FormForm, + // FormInput }, props: { endpoint: { @@ -14,6 +18,7 @@ export default { type: [Number, String], required: true }, + messageLayout: String, }, //TODO(Manu) endpoint macht Probleme data(){ @@ -32,12 +37,65 @@ export default { {title: "subject", field: "subject"}, {title: "body", field: "body", visible: false}, {title: "message_id", field: "message_id", visible: false}, - {title: "datum", field: "format_insertamum"}, + { + title: "Datum", + field: "insertamum", + formatter: function (cell) { + const dateStr = cell.getValue(); + const date = new Date(dateStr); // Convert to Date object + return date.toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false + }); + } + }, {title: "sender", field: "sender"}, {title: "recipient", field: "recipient"}, - {title: "sepersonid", field: "sepersonid"}, - {title: "repersonid", field: "repersonid"}, - {title: "status", field: "status"}, + {title: "senderId", field: "sender_id"}, + {title: "recipientId", field: "recipient_id"}, + { + title: "status", + field: "status", + formatter: function (cell) { + //TODO(Manu) get phrases in this context to work? + + /* const statusMap = { + 0: this.$p.t('messsages', 'unread'), + 1: this.$p.t('messsages', 'read'), + 2: this.$p.t('messsages', 'archived'), + 3: this.$p.t('messsages', 'deleted') + };*/ + const statusMap = { + 0: 'unread', + 1: 'read', + 2: 'archived', + 3: 'deleted' + }; + return statusMap[cell.getValue()]; + // return this.$p.t('messsages', 'deleted'); + } + + }, + { + title: "letzte Änderung", + field: "statusdatum", + formatter: function (cell) { + const dateStr = cell.getValue(); + const date = new Date(dateStr); // Convert to Date object + return date.toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false + }); + } + }, { title: 'Aktionen', field: 'actions', width: 100, @@ -47,23 +105,23 @@ export default { let button = document.createElement('button'); button.className = 'btn btn-outline-secondary btn-action'; - button.title = this.$p.t('ui', 'notiz_edit'); - button.innerHTML = ''; + button.title = this.$p.t('global', 'reply'); + button.innerHTML = ''; button.addEventListener( 'click', (event) => - this.actionEditNotiz(cell.getData().notiz_id) + this.reply(cell.getData().message_id) ); container.append(button); button = document.createElement('button'); button.className = 'btn btn-outline-secondary btn-action'; - button.title = this.$p.t('notiz', 'notiz_delete'); + button.title = this.$p.t('ui', 'loeschen'); button.innerHTML = ''; button.addEventListener( 'click', () => - this.actionDeleteNotiz(cell.getData().notiz_id) + this.deleteMessage(cell.getData().message_id) ); container.append(button); @@ -71,46 +129,186 @@ export default { }, frozen: true }], - layout: 'fitColumns', - layoutColumnsOnNewData: false, - height: '250', + layout: 'fitDataFill', + layoutColumnsOnNewData: false, + // height: 'auto', + height: '400', + selectable: true, + selectableRangeMode: 'click', +/* layoutColumnsOnNewData: false, + selectableRangeMode: 'click', selectable: true, index: 'message_id', - persistenceID: 'core-message' + persistenceID: 'core-message'*/ }, + tabulatorEvents: [ + { + event: 'dataLoaded', + handler: data => this.tabulatorData = data.map(item => { + return item; + }), + }, + { + event: 'tableBuilt', + handler: async() => { + await this.$p.loadCategory(['global', 'person', 'stv', 'messages', 'ui', 'notiz']); + + + let cm = this.$refs.table.tabulator.columnManager; + + cm.getColumnByField('subject').component.updateDefinition({ + title: this.$p.t('global', 'betreff') + }); + cm.getColumnByField('body').component.updateDefinition({ + title: this.$p.t('messages', 'body') + }); + cm.getColumnByField('message_id').component.updateDefinition({ + title: this.$p.t('messages', 'message_id') + }); + cm.getColumnByField('insertamum').component.updateDefinition({ + title: this.$p.t('global', 'datum') + }); + cm.getColumnByField('sender').component.updateDefinition({ + title: this.$p.t('messages', 'sender') + }); + cm.getColumnByField('recipient').component.updateDefinition({ + title: this.$p.t('messages', 'recipient') + }); + cm.getColumnByField('sender_id').component.updateDefinition({ + title: this.$p.t('messages', 'senderId') + }); + cm.getColumnByField('recipient_id').component.updateDefinition({ + title: this.$p.t('messages', 'recipientId') + }); + cm.getColumnByField('statusdatum').component.updateDefinition({ + title: this.$p.t('notiz', 'letzte_aenderung') + }); + /* + cm.getColumnByField('actions').component.updateDefinition({ + title: this.$p.t('global', 'aktionen') + }); + */ + } + }, + { + event: 'rowClick', + handler: (e, row) => { + const selectedMessage = row.getData().message_id; + const body = row.getData().body; + this.previewBody = body; + console.log(selectedMessage); + } + }, + ], + tabulatorData: [], + previewBody: "" } }, -/* computed: { + methods: { + reply(message_id){ + console.log("in reply " + message_id); + }, + deleteMessage(message_id){ + console.log("deleteMessage " + message_id); + }, + actionNewMessage(){ + console.log("action new message"); + }, + }, + computed: { statusText(){ - 0: this.$p.t('messsages', 'unread'), - 1: this.$p.t('messsages', 'read'), - 2: this.$p.t('messsages', 'archived'), - 0: this.$p.t('messsages', 'unread'), - 3: this.$p.t('person', 'deleted'), + return { + 0: this.$p.t('messsages', 'unread'), + 1: this.$p.t('messsages', 'read'), + 2: this.$p.t('messsages', 'archived'), + 3: this.$p.t('messsages', 'deleted') + } } - },*/ + }, + mounted() { + // change to target="_blank" +/* this.$nextTick(() => { + const links = document.querySelectorAll('.preview a'); + links.forEach(link => { + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); // Sicherheitsmaßnahme + }); + });*/ + }, template: `
-

Table Messages

-

type_id: {{typeId}}

-

id: {{id}}

endpoint: {{endpoint}}

+

{{messageLayout}}

+ + + + + + +
- - - - - +
+ +
+ + +
+ + +
+



+ +
+
+
+ +
+ +
+
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+ +
+
+
` diff --git a/public/js/components/Messages/Messages.js b/public/js/components/Messages/Messages.js index 6811a3aed..3fd6e2a02 100644 --- a/public/js/components/Messages/Messages.js +++ b/public/js/components/Messages/Messages.js @@ -17,14 +17,24 @@ export default { required: true }, showNew: Boolean, - showTable: Boolean + showTable: Boolean, + messageLayout: { + type: String, + default: 'twoColumnsTableLeft', + validator(value) { + return [ + 'twoColumnsTableLeft', + 'listTableTop' + ].includes(value) + } + }, }, data() { return {} }, template: `
-

endpoint: {{endpoint}}

+

endpoint Messages.js: {{endpoint}}

diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Messages.js b/public/js/components/Stv/Studentenverwaltung/Details/Messages.js index 7bd6d4b4d..e9f542945 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Messages.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Messages.js @@ -14,12 +14,11 @@ export default { endpoint="$fhcApi.factory.messages.person" type-id="person_id" :id="modelValue.person_id" + messageLayout="listTableTop" show-table > - -
` }; \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index f25cc72ec..95a505a23 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -37319,6 +37319,206 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'unread', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'ungelesen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'unread', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'read', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'gelesen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'read', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'archived', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'archiviert', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'archived', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'deleted', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'gelöscht', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'deleted', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'body', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nachrichtentext', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Body', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'message_id', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Message ID', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Message ID', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'sender', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'SenderIn', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Sender', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'recipient', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'EmpfängerIn', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Recipient', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'senderId', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'SenderIn ID', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Sender ID', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'messages', + 'phrase' => 'recipientId', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'EmpfängerIn ID', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Recipient ID', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), /////////// FHC4 Phrases Messages END /////////// ); From c960b4832d110794842de25d9c8caa5274dfff0a Mon Sep 17 00:00:00 2001 From: SimonGschnell Date: Mon, 17 Feb 2025 14:40:24 +0100 Subject: [PATCH 024/572] test --- public/js/api/stundenplan.js | 11 +++++++++++ public/js/apps/Dashboard/Fhc.js | 16 ++++++++-------- public/js/components/Calendar/Week/Page.js | 4 +++- public/js/components/Cis/Cms/Content.js | 2 +- public/js/components/Cis/Cms/News.js | 2 +- .../StudiengangInformation.js | 4 ++-- public/js/components/Cis/Mylv/LvUebersicht.js | 4 ++-- .../js/components/Cis/Stundenplan/Stundenplan.js | 5 +++++ 8 files changed, 33 insertions(+), 15 deletions(-) diff --git a/public/js/api/stundenplan.js b/public/js/api/stundenplan.js index 9ddbeff54..0f5de934e 100644 --- a/public/js/api/stundenplan.js +++ b/public/js/api/stundenplan.js @@ -42,4 +42,15 @@ export default { {} ); }, + getMoodleEventsByUserid(username, timestart, timeend) { + return this.$fhcApi.get( + FHC_JS_DATA_STORAGE_OBJECT.app_root + + `addons/moodle/cis/get_events_by_userid.php`, + { + username: username, + timestart: timestart, + timeend: timeend, + } + ); + }, }; \ No newline at end of file diff --git a/public/js/apps/Dashboard/Fhc.js b/public/js/apps/Dashboard/Fhc.js index 9f77d4b9b..19f6b3a2b 100644 --- a/public/js/apps/Dashboard/Fhc.js +++ b/public/js/apps/Dashboard/Fhc.js @@ -2,14 +2,14 @@ import FhcDashboard from '../../components/Dashboard/Dashboard.js'; import FhcApi from '../../plugin/FhcApi.js'; import Phrasen from '../../plugin/Phrasen.js'; import contrast from '../../directives/contrast.js'; -import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers"; -import Stundenplan from "../../components/Cis/Stundenplan/Stundenplan"; -import MylvStudent from "../../components/Cis/Mylv/Student"; -import Profil from "../../components/Cis/Profil/Profil"; -import CmsNews from "../../components/Cis/Cms/News"; -import CmsContent from "../../components/Cis/Cms/Content"; -import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info"; -import RoomInformation from "../../components/Cis/Mylv/RoomInformation"; +import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; +import Stundenplan from "../../components/Cis/Stundenplan/Stundenplan.js"; +import MylvStudent from "../../components/Cis/Mylv/Student.js"; +import Profil from "../../components/Cis/Profil/Profil.js"; +import CmsNews from "../../components/Cis/Cms/News.js"; +import CmsContent from "../../components/Cis/Cms/Content.js"; +import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js"; +import RoomInformation from "../../components/Cis/Mylv/RoomInformation.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; diff --git a/public/js/components/Calendar/Week/Page.js b/public/js/components/Calendar/Week/Page.js index 10ff6c495..b029b854a 100644 --- a/public/js/components/Calendar/Week/Page.js +++ b/public/js/components/Calendar/Week/Page.js @@ -197,7 +197,9 @@ export default { 'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)', 'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)', } - + console.log(this.smallestTimeFrame,"this is the smallest timeframe") + console.log(this.hours.length,"this is the length of the hours") + console.log(this.hours.length/60, "this is the length of the hours multiplied by 60 resulting in minutes") if(day.isPast) { styleObj['background-color'] = '#F5E9D7' styleObj['border-color'] = '#E8E8E8'; diff --git a/public/js/components/Cis/Cms/Content.js b/public/js/components/Cis/Cms/Content.js index ed87e1aae..d657322ba 100644 --- a/public/js/components/Cis/Cms/Content.js +++ b/public/js/components/Cis/Cms/Content.js @@ -1,6 +1,6 @@ import raum_contentmittitel from './Content_types/Raum_contentmittitel.js' import general from './Content_types/General.js' -import BsConfirm from "../../Bootstrap/Confirm"; +import BsConfirm from "../../Bootstrap/Confirm.js"; export default { name: "ContentComponent", diff --git a/public/js/components/Cis/Cms/News.js b/public/js/components/Cis/Cms/News.js index 89bed18d7..1567cbef4 100644 --- a/public/js/components/Cis/Cms/News.js +++ b/public/js/components/Cis/Cms/News.js @@ -1,6 +1,6 @@ import Pagination from "../../Pagination/Pagination.js"; import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js"; -import BsConfirm from "../../Bootstrap/Confirm"; +import BsConfirm from "../../Bootstrap/Confirm.js"; export default { name: "NewsComponent", diff --git a/public/js/components/Cis/Cms/StudiengangInformation/StudiengangInformation.js b/public/js/components/Cis/Cms/StudiengangInformation/StudiengangInformation.js index 06743ae5d..92e303801 100644 --- a/public/js/components/Cis/Cms/StudiengangInformation/StudiengangInformation.js +++ b/public/js/components/Cis/Cms/StudiengangInformation/StudiengangInformation.js @@ -1,5 +1,5 @@ -import StudiengangPerson from "./StudiengangPerson"; -import StudiengangVertretung from "./StudiengangVertretung"; +import StudiengangPerson from "./StudiengangPerson.js"; +import StudiengangVertretung from "./StudiengangVertretung.js"; export default { data(){ diff --git a/public/js/components/Cis/Mylv/LvUebersicht.js b/public/js/components/Cis/Mylv/LvUebersicht.js index 6670a0c76..13972af5e 100644 --- a/public/js/components/Cis/Mylv/LvUebersicht.js +++ b/public/js/components/Cis/Mylv/LvUebersicht.js @@ -1,5 +1,5 @@ -import BsModal from "../../Bootstrap/Modal"; -import LvMenu from "./LvMenu"; +import BsModal from "../../Bootstrap/Modal.js"; +import LvMenu from "./LvMenu.js"; export default { props:{ diff --git a/public/js/components/Cis/Stundenplan/Stundenplan.js b/public/js/components/Cis/Stundenplan/Stundenplan.js index d35c9c536..ba3a3c649 100644 --- a/public/js/components/Cis/Stundenplan/Stundenplan.js +++ b/public/js/components/Cis/Stundenplan/Stundenplan.js @@ -150,6 +150,11 @@ export const Stundenplan = { }, created() { + let time_start = Math.floor(this.eventCalendarDate.firstDayOfCalendarMonth.getTime() / 1000); + let time_end = Math.floor(this.eventCalendarDate.lastDayOfCalendarMonth.getTime() / 1000); + this.$fhcApi.factory.stundenplan.getMoodleEventsByUserid('io23m005', time_start, time_end).then((response) => { + console.log(response); + }) this.$fhcApi.factory.authinfo.getAuthUID().then((res) => res.data) .then(data=>{ this.uid = data.uid; From e086da2274ce66bc477d8ba5b6c8308b38eba720 Mon Sep 17 00:00:00 2001 From: SimonGschnell Date: Tue, 18 Feb 2025 14:23:56 +0100 Subject: [PATCH 025/572] feature(Calendar Moodle Events): displays moodle events as allDayEvents in the Calendar --- public/js/components/Calendar/Week/Page.js | 75 ++++++++++++++----- public/js/components/Cis/Mylv/LvModal.js | 4 +- .../components/Cis/Stundenplan/Stundenplan.js | 74 +++++++++++++----- 3 files changed, 116 insertions(+), 37 deletions(-) diff --git a/public/js/components/Calendar/Week/Page.js b/public/js/components/Calendar/Week/Page.js index b029b854a..504293d07 100644 --- a/public/js/components/Calendar/Week/Page.js +++ b/public/js/components/Calendar/Week/Page.js @@ -10,7 +10,7 @@ export default { hourPosition:null, hourPositionTime:null, resizeObserver: null, - width: 0 + width: 0, } }, inject: [ @@ -36,6 +36,16 @@ export default { 'input', ], computed: { + allDayEvents(){ + let allDayEvents = {}; + for(let day in this.events){ + const filteredAllDayEvents = this.events[day].filter(event=>event.allDayEvent); + if (filteredAllDayEvents.length > 0){ + allDayEvents[day]=filteredAllDayEvents; + } + }; + return allDayEvents; + }, getGridStyle() { return { 'min-height': '100px', @@ -126,6 +136,7 @@ export default { d.isToday = nextDay.getFullYear() === this.todayDate.getFullYear() && nextDay.getMonth() === this.todayDate.getMonth() && nextDay.getDate() === this.todayDate.getDate() if (this.events[key]) { this.events[key].forEach(evt => { + if (evt.allDayEvent) return; let event = {orig:evt,lane:1,maxLane:1,start: evt.start < day ? day : evt.start, end: evt.end > nextDay ? nextDay : evt.end,shared:[],setSharedMaxRecursive(doneItems) { this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane); doneItems.push(this); @@ -197,31 +208,44 @@ export default { 'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)', 'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)', } - console.log(this.smallestTimeFrame,"this is the smallest timeframe") - console.log(this.hours.length,"this is the length of the hours") - console.log(this.hours.length/60, "this is the length of the hours multiplied by 60 resulting in minutes") if(day.isPast) { styleObj['background-color'] = '#F5E9D7' styleObj['border-color'] = '#E8E8E8'; styleObj.opacity = 0.5; } else if (day.isToday) { - // styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)' - // styleObj['border-color'] = '#E8E8E8'; - // styleObj.opacity = 0.5; + styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)' + styleObj['border-color'] = '#E8E8E8'; + styleObj.opacity = 0.5; } return styleObj }, eventGridStyle(day, event) { - return { - 'z-index': 1, - 'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane, - 'grid-column-end': 1 + event.lane * day.lanes / event.maxLane, - 'grid-row-start': this.dateToMinutesOfDay(event.start), - 'grid-row-end': this.dateToMinutesOfDay(event.end), - 'background-color': event.orig.color, - 'max-height': '75px' + if (event.orig.allDayEvent) + { + return; + return { + 'z-index': '2', + 'grid-column': '1 / -1', + 'background-color': 'rgb(204, 204, 204)', + 'max-height': '75px', + color: 'black', + position: 'sticky', + top: '44px', + }; + } + else + { + return { + 'z-index': 1, + 'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane, + 'grid-column-end': 1 + event.lane * day.lanes / event.maxLane, + 'grid-row-start': this.dateToMinutesOfDay(event.start), + 'grid-row-end': this.dateToMinutesOfDay(event.end), + 'background-color': event.orig.color, + 'max-height': '75px' + }; } }, calcHourPosition(event) { @@ -333,12 +357,27 @@ export default { {{hourPositionTime}}
- +
+
{{hour}}:00
-
+
+
+
+
+ +

this is a placeholder which means that no template was passed to the Calendar Page slot

+
+
+
+
{{curTime}} @@ -350,7 +389,7 @@ export default { :style="eventGridStyle(day,event)" class="mx-2 small rounded overflow-hidden fhc-entry " v-contrast > - +

this is a placeholder which means that no template was passed to the Calendar Page slot

diff --git a/public/js/components/Cis/Mylv/LvModal.js b/public/js/components/Cis/Mylv/LvModal.js index e873dd5f8..acb2a7693 100644 --- a/public/js/components/Cis/Mylv/LvModal.js +++ b/public/js/components/Cis/Mylv/LvModal.js @@ -35,7 +35,7 @@ export default { }, data() { return { - menu: [], + menu: null, result: false, info: null, }; @@ -76,7 +76,7 @@ export default {