diff --git a/application/config/search.php b/application/config/search.php new file mode 100644 index 000000000..bedf8d888 --- /dev/null +++ b/application/config/search.php @@ -0,0 +1,874 @@ + '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' => [ + "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", + "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)" +]; + +$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", + "(s.student_uid || '@" . DOMAIN . "') || ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email", + "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", + "b.aktiv" + ], + '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)" +]; + +$prestudent_sort = [ + "Student", + "Incoming", + "Outgoing", + "Diplomand", + "Unterbrecher", + "Aufgenommener", + "Wartender", + "Bewerber", + "Interessent", + "Abgewiesener", + "Absolvent", + "Abbrecher", + "Ausserordentlicher", + "Praktikant" +]; +$prestudent_sort_array = "array['" . implode("','", $prestudent_sort) . "']"; +$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", + "(b.uid || '@" . DOMAIN . "') || ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email", + "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", + "( + SELECT bezeichnung_mehrsprachig[(TABLE lang)] + FROM public.tbl_status + WHERE status_kurzbz = public.get_rolle_prestudent(ps.prestudent_id, NULL) + LIMIT 1 + ) 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", + "b.aktiv", + "array_position(" . $prestudent_sort_array . ", public.get_rolle_prestudent(ps.prestudent_id, NULL)) AS sort" + ], + '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", + 'join' => [ + 'table' => "public.tbl_benutzer", + 'on' => "uid = mitarbeiter_uid" + ] + ], + '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)" +]; + +// TODO(chris): move to searchpv21.php +$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['unassigned_employee']['renderer'] = 'employee'; + +$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)" +]; +$sprache = getUserLanguage(); +$config['cms'] = [ + 'primarykey' => 'contentsprache_id', + 'table' => 'campus.tbl_contentsprache', + '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') + ) + ", + 'searchfields' => [ + 'content' => [ + 'alias' => ['inhalt'], + 'comparison' => "vector", + '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" + ], + 'lang' => [ + 'alias' => ['language', 'sprache'], + 'comparison' => "equals", + 'field' => "sprache" + ] + ], + '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) + 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) + AND contentsprache.sprache = '{$sprache}'" +]; + +$config['dms'] = [ + '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/application/config/searchcis.php b/application/config/searchcis.php new file mode 100644 index 000000000..ce385338f --- /dev/null +++ b/application/config/searchcis.php @@ -0,0 +1,35 @@ +config->item('employee', 'search'); + +$config['student'] = $CI->config->item('student', 'search'); +unset($config['student']['searchfields']['email']); +unset($config['student']['searchfields']['tel']); +$config['student']['resultfields'] = [ + "s.student_uid AS uid", + "s.matrikelnr", + "p.person_id", + "(p.vorname || ' ' || p.nachname) AS name", + "ARRAY[s.student_uid || '@' || '" . DOMAIN . "'] AS email", + "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", + "b.aktiv" +]; + +$config['organisationunit'] = $CI->config->item('organisationunit', 'search'); +$config['organisationunit']['prepare'] = 'active_organisationseinheit AS (SELECT * FROM public.tbl_organisationseinheit WHERE aktiv = true)'; +$config['organisationunit']['table'] = 'active_organisationseinheit'; + +$config['room'] = $CI->config->item('room', 'search'); + +$config['cms'] = $CI->config->item('cms', 'search'); + +$config['dms'] = $CI->config->item('dms', 'search'); diff --git a/application/config/searchfunctions.php b/application/config/searchfunctions.php new file mode 100644 index 000000000..276652997 --- /dev/null +++ b/application/config/searchfunctions.php @@ -0,0 +1,35 @@ + 4, + 'rank' => "0", + 'compare' => "{field}::text = {word}::text", + '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({field}, to_tsquery('simple', {word}))", + 'compare' => "to_tsquery('simple', {word}) @@ {field}" +]; + diff --git a/application/config/searchstv.php b/application/config/searchstv.php new file mode 100644 index 000000000..96c118ac8 --- /dev/null +++ b/application/config/searchstv.php @@ -0,0 +1,11 @@ +config->item('student', 'search'); + +$config['prestudent'] = $CI->config->item('prestudent', 'search'); 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/controllers/api/frontend/v1/Searchbar.php b/application/controllers/api/frontend/v1/Searchbar.php index 8b383e042..363b6e534 100644 --- a/application/controllers/api/frontend/v1/Searchbar.php +++ b/application/controllers/api/frontend/v1/Searchbar.php @@ -35,11 +35,10 @@ class Searchbar extends FHCAPI_Controller { // NOTE(chris): additional permission checks will be done in SearchBarLib parent::__construct([ - 'search' => self::PERM_LOGGED + 'search' => self::PERM_LOGGED, + 'searchCis' => self::PERM_LOGGED, + 'searchStv' => self::PERM_LOGGED ]); - - // Load the library SearchBarLib - $this->load->library('SearchBarLib'); } //------------------------------------------------------------------------------------------------------------------ @@ -50,6 +49,7 @@ class Searchbar extends FHCAPI_Controller */ public function search() { + $this->load->library('SearchBarLib'); $this->load->library('form_validation'); // Checks if the searchstr and the types parameters are in the POSTed JSON @@ -63,7 +63,53 @@ class Searchbar extends FHCAPI_Controller $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); + + $this->addMeta('mode', 'simple'); + + $this->terminateWithSuccess($result->data); + } + + /** + * Gets a JSON body via HTTP POST and provides the parameters + */ + public function searchCis() + { + return $this->searchAdvanced([ 'config' => 'searchcis' ]); + } + + /** + * Gets a JSON body via HTTP POST and provides the parameters + */ + public function searchStv() + { + return $this->searchAdvanced([ 'config' => 'searchstv' ]); + } + + /** + * Gets a JSON body via HTTP POST and provides the parameters + */ + private function searchAdvanced($config) + { + $this->load->library('SearchLib', $config); + $this->load->library('form_validation'); + + // Checks if the searchstr and the types parameters are in the POSTed JSON + $this->form_validation->set_rules(self::SEARCHSTR_PARAM, null, 'required'); + $this->form_validation->set_rules(self::TYPES_PARAM . '[]', null, 'required'); + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + // Convert to json the result from searchlib->search + $result = $this->searchlib->search($this->input->post(self::SEARCHSTR_PARAM), $this->input->post(self::TYPES_PARAM)); + + $data = $this->getDataOrTerminateWithError($result); + + $this->addMeta('time', $result->meta['time']); + $this->addMeta('searchstring', $result->meta['searchstring']); + $this->addMeta('mode', 'advanced'); + + $this->terminateWithSuccess($data); } } diff --git a/application/core/FHCAPI_Controller.php b/application/core/FHCAPI_Controller.php index 6697953bc..dad56334d 100644 --- a/application/core/FHCAPI_Controller.php +++ b/application/core/FHCAPI_Controller.php @@ -109,10 +109,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/SearchLib.php b/application/libraries/SearchLib.php new file mode 100644 index 000000000..24894eab5 --- /dev/null +++ b/application/libraries/SearchLib.php @@ -0,0 +1,1091 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +use \stdClass as stdClass; + +/** + * This is a alternative for SearchBarLib for advanced searches + */ +class SearchLib +{ + // Error constats + const ERROR_WRONG_JSON = 'ERR001'; + const ERROR_WRONG_SEARCHSTR = 'ERR002'; + const ERROR_NO_TYPES = 'ERR003'; + const ERROR_WRONG_TYPES = 'ERR004'; + const ERROR_NOT_AUTH = 'ERR005'; + + private $_ci; // Code igniter instance + + private $_searchfunction_priorities = []; + private $_numeric_searchfunctions = []; + private $_allowed_searchfunctions = []; + + /** + * Gets the CI instance and loads model + * + * @param array $params + * @return void + */ + public function __construct($params = null) + { + $this->_ci =& get_instance(); // get code igniter instance + + $config = $params['config'] ?? null; + // 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, (boolean)$config); + $this->_ci->load->config('searchfunctions', true); + if ($config) { + $this->_ci->load->config($config, true); + $this->_ci->config->set_item('search', $this->_ci->config->item($config)); + } + + $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; + } + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + /** + * It performes the search of the given search string using the specified search types + * + * @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($searchstring, $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 + ]); + }, $missing)); + } + $types = $tmp; + } + + + // 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"; + 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) . " + LIMIT 1 + ), + auth (uid) AS ( + SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid + )"; + + 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 (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( + SELECT " . $this->_formatPrimarykeys($table_config['primarykey']) . ", MAX(rank) + FROM (" . implode(" UNION ", $sql_select) . ") q + GROUP BY " . $this->_formatPrimarykeys($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 . " (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( + SELECT " . $this->_formatPrimarykeys($table_config['primarykey']) . ", MAX(rank) + FROM (" . implode(" UNION ", $select) . ") q + GROUP BY " . $this->_formatPrimarykeys($table_config['primarykey']) . " + )"; + + $renderer = $table_config['renderer'] ?? $type; + $selects[] = " + SELECT + " . $this->_ci->db->escape($renderer) . " AS renderer, + " . $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(""); + + $recursive = ""; + if ($with && $with[0] === "RECURSIVE") { + $recursive = "RECURSIVE "; + array_shift($with); + } + + $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 + )"); + array_unshift($with, "auth (uid) AS ( + SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid + )"); + + return success(" + WITH " . $recursive . 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 + ])); + + $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' + ]); + } + 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' + ]); + } + 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'])) { + $this->_addPreparesToSqlWith($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'])) { + $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 + " . $this->_formatPrimarykeys($table_config['primarykey'], $table_config['table']) . " + FROM " . $table_config['table'] . " + " . $this->_makeJoin($field_config['join'] ?? '') . " + WHERE "; + foreach ($words as $word) { + $sql[] = $field_sql . $this->_makeCompareBool($field_config['comparison'], $field_config['field'], $word); + } + } + + $or_select[] = " + SELECT + " . $this->_formatPrimarykeys($table_config['primarykey'], $table_config['table']) . ", + 1.0 AS rank + FROM " . $table_config['table'] . " + WHERE " . $table_config['primarykey'] . " 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 ($this->_needBasicTableJoin($field_config['field'], $table_config['primarykey'])) { + $word_join .= " " . $this->_makeJoin($table_config); + } + $word_rank = "rank"; + } + if (isset($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']); + } + if (isset($field_config['join'])) { + $word_join .= " " . $this->_makeJoin($field_config['join']); + } + $field_sql[] = " + SELECT + " . $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 . " + " . $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 . " (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( + SELECT + " . $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 " . $this->_formatPrimarykeys($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 " . + $this->_formatPrimarykeys($table_config['primarykey'], $current_select ?: $table_config['table']) . + ", 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 ($this->_needBasicTableJoin($field_config['field'], $table_config['primarykey'])) { + $word_join .= " " . $this->_makeJoin($table_config); + } + $word_rank = "rank + "; + } + if (isset($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']); + } + if (isset($field_config['join'])) { + $word_join .= " " . $this->_makeJoin($field_config['join'], $jointype); + } + + $id = "w" . ($id_offset + count($or_with)); + $or_with[] = " + " . $id . " (" . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank) AS ( + SELECT + " . $this->_formatPrimarykeys($table_config['primarykey'], $word_from) . ", + " . $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 " . $this->_formatPrimarykeys($table_config['primarykey']) . ", rank / " . $count . " AS rank FROM " . $current_select; + } + + if ($or_with[0] === "RECURSIVE") + { + if (empty($sqlWith) || $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); + } + + return $sql_select; + } + + //------------------------------------------------------------------------------------------------------------------ + // Private methods + + /** + * Checks if the field is not one of the primarykeys. + * + * @param string $field + * @param array|string $primarykeys + * + * @return boolean + */ + private function _needBasicTableJoin($field, $primarykeys) + { + 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; + } + + /** + * Returns comma separated primarykeys. Optionally with table prefix + * + * @param array|string $primarykeys + * @param string $prefix + * + * @return string + */ + private function _formatPrimarykeys($primarykeys, $prefix = "") + { + if (is_array($primarykeys)) { + if ($prefix) + $prefix .= "."; + return $prefix . implode(", " . $prefix, $primarykeys); + } + if (!$prefix) + return $primarykeys; + + return $prefix . "." . implode(", " . $prefix . ".", explode(",", $primarykeys)); + } + + /** + * Adds the prepare statement to the sqlWith stack and handles the + * "RECURSIVE" modifier + * + * @param array &$sqlWith + * @param array $prepares + * + * @return void + */ + private function _addPreparesToSqlWith(&$sqlWith, $prepares) + { + $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"); + } + } + + /** + * 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->escape_like_str($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/searchbar.css b/public/css/components/searchbar/searchbar.css index 867273962..09da19625 100644 --- a/public/css/components/searchbar/searchbar.css +++ b/public/css/components/searchbar/searchbar.css @@ -14,6 +14,35 @@ --fhc-searchbar-results-btn-color: var(--bs-btn-color, white); } +.searchbar_input_clear { + /* 1rem * 5 / 8 = width of icon */ + margin-inline-start: calc(-1rem * 5 / 8 - var(--bs-btn-padding-x) * 2 - var(--bs-border-width)) !important; + border: 0; + margin-inline-end: 1px; + z-index: 1; +} +.searchbar_input_clear:hover, +.searchbar_input_clear:focus { + color: var(--bs-body-color); + background-color: transparent; +} +.searchbar_input:focus, +.searchbar_input:focus ~ .searchbar_input_clear { + z-index: 5; /* same as bootstrap .input-group > .form-control:focus */ +} + +.searchbar_setting_btn { + --bs-btn-color: var(--bs-body-color); + --bs-btn-bg: var(--bs-tertiary-bg); + --bs-btn-border-color: var(--bs-border-color); +} + + +.searchbar_searchbox.open { + z-index: 10000; +} + + .searchbar_settings { position: absolute; z-index: 9999; @@ -120,6 +149,76 @@ list-style: none; } +/* new variant with template/frame */ + +.searchbar-result-template-frame { + border-bottom: 1px solid lightgrey; + margin-bottom: 1rem; + padding-bottom: 1rem; +} + +.searchbar-result-template-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; +} +.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), 1); +} +.searchbar-square-image.rounded-circle > img, +.searchbar-square-image.rounded-circle > div { + border-radius: 50%; +} + +.searchbar-rounded-image { + display: block; + width: 100%; +} +.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), 1); + height: calc((100px - .75em) * 1.3); +} +.searchbar-rounded-image > img, +.searchbar-rounded-image > div { + border-radius: .25rem; +} + +.no-margin-paragraphs p { + margin: 0; +} + .non-selectable { user-select: none; -webkit-user-select: none; /* Safari */ @@ -129,4 +228,4 @@ .searchbar_inaktiv { opacity: .6; -} \ No newline at end of file +} diff --git a/public/js/api/factory/language.js b/public/js/api/factory/language.js new file mode 100644 index 000000000..8175ccc0e --- /dev/null +++ b/public/js/api/factory/language.js @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getAll() { + return { + method: 'get', + url: '/api/frontend/v1/language/get' + }; + } +}; \ No newline at end of file diff --git a/public/js/api/factory/searchbar.js b/public/js/api/factory/searchbar.js index a7bd73b88..6bcf577b4 100644 --- a/public/js/api/factory/searchbar.js +++ b/public/js/api/factory/searchbar.js @@ -22,5 +22,19 @@ export default { url: '/api/frontend/v1/searchbar/search', params }; + }, + searchCis(params) { + return { + method: 'post', + url: '/api/frontend/v1/searchbar/searchCis', + params + }; + }, + searchStv(params) { + return { + method: 'post', + url: '/api/frontend/v1/searchbar/searchStv', + params + }; } }; \ No newline at end of file diff --git a/public/js/api/fhcapifactory.js b/public/js/api/fhcapifactory.js index 1b9d020dd..63ad680ad 100644 --- a/public/js/api/fhcapifactory.js +++ b/public/js/api/fhcapifactory.js @@ -40,6 +40,7 @@ import menu from "./menu.js"; import dashboard from "./dashboard.js"; import authinfo from "./authinfo.js"; import studium from "./studium.js"; +import language from "./language.js"; export default { search, @@ -68,4 +69,5 @@ export default { menu, authinfo, studium, + 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/api/search.js b/public/js/api/search.js index f01551af2..49623398e 100644 --- a/public/js/api/search.js +++ b/public/js/api/search.js @@ -16,8 +16,16 @@ */ export default { - search(searchsettings) { + search(searchsettings, config) { const url = '/api/frontend/v1/searchbar/search'; + return this.$fhcApi.post(url, searchsettings, config); + }, + searchAdvanced(searchsettings, config) { + const url = '/api/frontend/v1/searchbar/searchAdvanced'; + return this.$fhcApi.post(url, searchsettings, config); + }, + searchdummy(searchsettings) { + const url = 'public/js/apps/api/dummyapi.php/Search'; return this.$fhcApi.post(url, searchsettings); } }; diff --git a/public/js/apps/Cis.js b/public/js/apps/Cis.js index b9280f738..6a5418c96 100644 --- a/public/js/apps/Cis.js +++ b/public/js/apps/Cis.js @@ -16,12 +16,14 @@ const app = Vue.createApp({ origin: "cis", cssclass: "", calcheightonly: true, - types: [ - "mitarbeiter", - "student", - "raum", - "organisationunit" - ], + types: { + employee: Vue.computed(() => this.$p.t("search/type_employee")), + student: Vue.computed(() => this.$p.t("search/type_student")), + room: Vue.computed(() => this.$p.t("search/type_room")), + organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")), + cms: Vue.computed(() => this.$p.t("search/type_cms")), + dms: Vue.computed(() => this.$p.t("search/type_dms")) + }, actions: { employee: { defaultaction: { @@ -44,7 +46,7 @@ const app = Vue.createApp({ }, childactions: [] }, - raum: { + room: { defaultaction: { type: "link", renderif: function(data) { @@ -106,14 +108,37 @@ const app = Vue.createApp({ } }, childactions: [] - } + }, + cms: { + defaultaction: { + type: "link", + action: function (data) { + const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + '/CisVue/Cms/content/' + data.content_id; + return link; + } + }, + childactions: [] + }, + dms: { + defaultaction: { + type: "link", + action: function (data) { + const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + + 'cms/dms.php?id=' + data.dms_id; + return link; + } + }, + childactions: [] + } } } }; }, methods: { searchfunction: function(searchsettings) { - return this.$api.call(ApiSearchbar.search(searchsettings)); + return this.$api.call(ApiSearchbar.searchCis(searchsettings)); } } }); diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index 96846f76d..6fa626695 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -75,12 +75,12 @@ export default { return { selected: [], searchbaroptions: { - cssclass: "position-relative", + origin: 'stv', calcheightonly: true, - types: [ - "studentStv", - "prestudent" - ], + types: { + student: Vue.computed(() => this.$p.t('search/type_student')), + prestudent: Vue.computed(() => this.$p.t('search/type_prestudent')) + }, actions: { student: { defaultaction: { @@ -101,8 +101,28 @@ export default { }, childactions: [ ] + }, + mergedPerson: { + defaultaction: { + type: "link", + action: data => this.$fhcApi.getUri('/studentenverwaltung/person/' + data.person_id) + }, + defaultactionstudent: { + type: "link", + action: data => { + if (data.prestudent_id) { + return this.$fhcApi.getUri('/studentenverwaltung/prestudent/' + data.prestudent_id); + } else if (data.uid) { + return this.$fhcApi.getUri('/studentenverwaltung/student/' + data.uid); + } else { + return this.$fhcApi.getUri('/studentenverwaltung/person/' + data.person_id); + } + } + }, + childactions: [] } - } + }, + mergeResults: 'person' }, studiengangKz: undefined, studiensemesterKurzbz: this.defaultSemester, @@ -128,8 +148,8 @@ export default { reloadList() { this.$refs.stvList.reload(); }, - searchfunction(params) { - return this.$api.call(ApiSearchbar.search(params)); + searchfunction(params, config) { + return this.$api.call(ApiSearchbar.searchStv(params), config); } }, created() { @@ -217,7 +237,11 @@ export default {
diff --git a/public/js/components/searchbar/action.js b/public/js/components/searchbar/action.js deleted file mode 100644 index a19ecfa97..000000000 --- a/public/js/components/searchbar/action.js +++ /dev/null @@ -1,44 +0,0 @@ -export default { - props: { - res: { - type: Object - }, - action: { - type: Object - }, - cssclass: { - type: String, - default: '' - } - }, - emits: [ 'actionexecuted' ], - template: ` - - - `, - 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'); - }, - renderif: function() { - if(this.action?.renderif === undefined) { - return true; - } - - return this.action.renderif(this.res); - } - } -}; \ 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 0fd164cea..000000000 --- a/public/js/components/searchbar/actions.js +++ /dev/null @@ -1,37 +0,0 @@ -import action from "./action.js"; - -export default { - props: [ "res", "actions" ], - components: { - action: action - }, - emits: [ 'actionexecuted' ], - template: ` -
    - -
-
- `, - methods: { - hasicon: function(index) { - return (typeof this.actions[index].icon !== "undefined"); - }, - geticonclass: function(index) { - return this.actions[index].icon; - }, - renderif: function(action) { - if(action?.renderif === undefined) { - return true; - } - - return action.renderif(this.res); - } - } -}; \ 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 65eb93d1f..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 18e11c861..000000000 --- a/public/js/components/searchbar/organisationunit.js +++ /dev/null @@ -1,83 +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 8e74a714f..000000000 --- a/public/js/components/searchbar/person.js +++ /dev/null @@ -1,56 +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 2887924e5..000000000 --- a/public/js/components/searchbar/prestudent.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 }} - - -
- -
- -
-
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 a64251356..000000000 --- a/public/js/components/searchbar/raum.js +++ /dev/null @@ -1,54 +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: /*html*/` -
-
-
- - - -
-
- - {{ res.ort_kurzbz }} - - -
- -
-
-
Standort
-
{{ res.standort }}
-
-
-
Sitzplätze
-
{{ res.sitzplaetze }}
-
-
-
Gebäude
-
{{ res.building }}
-
-
-
Zusatz Informationen
-
-
-
- - - -
-
- -
- `, - methods: { - } -}; \ 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..dfd2b575a --- /dev/null +++ b/public/js/components/searchbar/result/cms.js @@ -0,0 +1,75 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultCms', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + inject: [ + '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 + ''; + } + }, + template: ` + +
+
+ {{ $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 new file mode 100644 index 000000000..adb0172a3 --- /dev/null +++ b/public/js/components/searchbar/result/dms.js @@ -0,0 +1,75 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultDms', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + 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: ` + +
+
+
{{ $p.t('search/result_dms_id') }}
+
+ {{ res.dms_id }} +
+
+
+
{{ $p.t('search/result_version') }}
+
+ {{ res.version }} +
+
+
+
{{ $p.t('search/result_keywords') }}
+
+ {{ res.keywords }} +
+
+
+
{{ $p.t('global/beschreibung') }}
+
+ {{ res.description }} +
+
+
+
` +}; \ No newline at end of file diff --git a/public/js/components/searchbar/result/employee.js b/public/js/components/searchbar/result/employee.js new file mode 100644 index 000000000..d9d2332a5 --- /dev/null +++ b/public/js/components/searchbar/result/employee.js @@ -0,0 +1,60 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultEmployee', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + template: ` + +
+
+
{{ $p.t('search/result_stdkst') }}
+
+
    +
  • {{ stdkst }}
  • +
+ {{ $p.t('search/result_stdkst_none') }} +
+
+
+
{{ $p.t('lehre/organisationseinheit') }}
+
+
    +
  • {{ oe }}
  • +
+ {{ $p.t('search/result_oe_none') }} +
+
+
+
{{ $p.t('search/result_emails') }}
+ +
+
+
{{ $p.t('person/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..2b3397fde --- /dev/null +++ b/public/js/components/searchbar/result/mergedperson.js @@ -0,0 +1,209 @@ +import TemplateFrame from "./template/frame.js"; +import TemplateAction from "./template/action.js"; + +export default { + name: 'SearchbarResultMergedperson', + components: { + TemplateFrame, + TemplateAction + }, + emits: [ 'actionexecuted' ], + props: { + mode: String, + res: Object, + actions: Object + }, + computed: { + person() { + // 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) + let photo_url; + if (this.mode == 'simple') { + let foto = (this.students ? this.students.find(el => el.foto) : null)?.foto; + if (foto) + foto = 'data:image/jpeg;base64,' + foto; + photo_url = foto || this.employee?.photo_url; + } else + photo_url = ((this.students ? this.students.find(el => el.photo_url) : null) || this.employee)?.photo_url; + + return { person_id, name, photo_url, email }; + }, + employee() { + return this.res.list.find(item => [ + 'employee', + 'unassigned_employee', + 'mitarbeiter', + 'mitarbeiter_ohne_zuordnung' + ].includes(item.type)) || null; + }, + students() { + const students = this.res.list.filter(item => [ + 'student', + 'prestudent', + 'studentcis', + 'studentStv' + ].includes(item.type)) + .filter((item, idx, arr) => { + if (item.type === 'prestudent') { + return true; + } + + let prestudentwithsameuidexists = arr.some(tmpitem => { + return tmpitem.uid === item.uid && tmpitem.type === 'prestudent'; + }); + + if (prestudentwithsameuidexists) { + return false; + } + return true; + }).sort((a, b) => (a.sort || 0) - (b.sort || 0)); + return students.length ? students : null; + }, + emails() { + // Remove duplicates + return new Set(this.person.email); + }, + telurl() { + return 'tel:' + this.employee?.phone; + }, + inaktiv() { + return this.res.list.some(item => item?.aktiv === false); + } + }, + template: ` + +
+
+
{{ $p.t('person/person_id') }}
+
+ {{ person.person_id }} +
+
+
+
{{ $p.t('search/result_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..7c59b0bf8 --- /dev/null +++ b/public/js/components/searchbar/result/mergedstudent.js @@ -0,0 +1,39 @@ +import ResultPrestudent from "./prestudent.js"; +import ResultStudent from "./student.js"; + +export default { +name: 'SearchbarResultMergedstudent', + components: { + ResultPrestudent, + ResultStudent + }, + emits: [ 'actionexecuted' ], + props: { + mode: String, + 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..d0b191da4 --- /dev/null +++ b/public/js/components/searchbar/result/organisationunit.js @@ -0,0 +1,58 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultOrganistationunit', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + template: ` + +
+
+
{{ $p.t('search/result_parent_oe') }}
+
+ {{ res.parentoe_name }} +
+
+ +
+
{{ $p.t('search/result_group_emails') }}
+ +
+ +
+
{{ $p.t('search/result_leader') }}
+
+
    +
  • {{ leader.name }}
  • +
+ {{ $p.t('search/result_leader_none') }} +
+
+ +
+
{{ $p.t('search/result_number_of_employees') }}
+
+ {{ 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..da920be2c --- /dev/null +++ b/public/js/components/searchbar/result/person.js @@ -0,0 +1,45 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultPerson', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + res: Object, + actions: Object + }, + computed: { + emails() { + return new Set(this.res.email); + } + }, + template: ` + +
+
+
{{ $p.t('person/person_id') }}
+
+ {{ res.person_id }} +
+
+
+
{{ $p.t('search/result_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..f142fcdaa --- /dev/null +++ b/public/js/components/searchbar/result/prestudent.js @@ -0,0 +1,84 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultPrestudent', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + mode: String, + res: Object, + actions: Object + }, + computed: { + title() { + if (this.mode == 'simple') + return this.res.name; + return this.res.name + ' (' + this.res.status + ' ' + this.res.stg_kuerzel + ')'; + }, + photo_url() { + if (this.mode != 'simple') + return this.res.photo_url; + if (this.res.foto) + return 'data:image/jpeg;base64,' + this.res.foto; + return null; + }, + emails() { + if (this.mode == 'simple') + return new Set([this.res.email]); + return new Set(this.res.email); + } + }, + template: ` + +
+
+
{{ $p.t('person/person_id') }}
+
+ {{ res.person_id }} +
+
+
+
{{ $p.t('search/result_emails') }}
+ +
+
+
{{ $p.t('search/result_student_uid') }}
+
+ {{ res.uid }} +
+
+
+
{{ $p.t('person/matrikelnummer') }}
+
+ {{ res.matrikelnr }} +
+
+
+
{{ $p.t('search/result_prestudent_id') }}
+
+ {{ res.prestudent_id }} +
+
+
+
{{ $p.t('lehre/studiengang') }}
+
+ {{ res.bezeichnung }} {{ res.orgform ? '(' + res.orgform + ')' : '' }} +
+
+
+
` +}; \ 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..39e50395d --- /dev/null +++ b/public/js/components/searchbar/result/room.js @@ -0,0 +1,80 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultRoom', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + mode: String, + 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.$p.t('search/result_address_floor', this.res); + + return address || this.$p.t('search/result_address_none'); + } + }, + template: ` + +
+
+
{{ $p.t('search/result_room_address') }}
+
+ {{ address }} +
+
+ +
+
{{ $p.t('search/result_workplaces') }}
+
+ {{ res.sitzplaetze }} +
+
+ + +
+
+ +
+
{{ $p.t('search/result_building') }}
+
+ {{ res.building }} +
+
+ +
+
{{ $p.t('search/result_equipment') }}
+
+
+
+
+
+
+
` +}; \ 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..db31bb689 --- /dev/null +++ b/public/js/components/searchbar/result/student.js @@ -0,0 +1,67 @@ +import TemplateFrame from "./template/frame.js"; + +export default { + name: 'SearchbarResultStudent', + components: { + TemplateFrame + }, + emits: [ 'actionexecuted' ], + props: { + mode: String, + res: Object, + actions: Object + }, + computed: { + photo_url() { + if (this.mode != 'simple') + return this.res.photo_url; + if (this.res.foto) + return 'data:image/jpeg;base64,' + this.res.foto; + return null; + }, + emails() { + if (this.mode == 'simple') + return new Set([this.res.email]); + return new Set(this.res.email); + } + }, + template: ` + +
+
+
{{ $p.t('search/result_student_uid') }}
+
+ {{ res.uid }} +
+
+
+
{{ $p.t('person/person_id') }}
+
+ {{ res.person_id }} +
+
+
+
{{ $p.t('person/matrikelnummer') }}
+
+ {{ res.matrikelnr }} +
+
+
+
{{ $p.t('search/result_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..7c50eb134 --- /dev/null +++ b/public/js/components/searchbar/result/template/action.js @@ -0,0 +1,29 @@ +export default { + name: 'SearchbarResultTemplateAction', + 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: ` + + {{ $p.t('search/action_default_label') }} + ` +}; \ 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..726fef4dd --- /dev/null +++ b/public/js/components/searchbar/result/template/actions.js @@ -0,0 +1,27 @@ +import ResultAction from "./action.js"; + +export default { + name: 'SearchbarResultTemplateActions', + 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..5c8f8aa02 --- /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 { + name: 'SearchbarResultTemplateFrame', + 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 197800c29..6a41fe2f5 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -1,137 +1,206 @@ -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 person from "./result/person.js"; +import room from "./result/room.js"; +import employee from "./result/employee.js"; +import organisationunit from "./result/organisationunit.js"; +import student from "./result/student.js"; +import prestudent from "./result/prestudent.js"; +import dms from "./result/dms.js"; +import cms from "./result/cms.js"; +import mergedStudent from "./result/mergedstudent.js"; +import mergedPerson from "./result/mergedperson.js"; export default { + name: "FhcSearchbar", + components: { + person, + room, + employee, + organisationunit, + student, + prestudent, + dms, + cms, + mergedStudent, + mergedPerson + }, props: [ "searchoptions", "searchfunction" ], + provide() { + return { + query: Vue.computed(() => this.lastQuery) + }; + }, data: function() { return { searchtimer: null, hidetimer: null, searchsettings: { searchstr: this.getSearchStr(), - types: this.getSearchTypes(), + types: this.getInitiallySelectedTypes(), }, searchresult: [], + searchmode: '', showresult: false, searching: false, error: null, - settingsDropdown:null, + abortController: null, + settingsDropdown: null, + lastQuery: '' }; }, - components: { - person: person, - raum: raum, - employee: employee, - organisationunit: organisationunit, - student: student, - prestudent: prestudent - }, - template: /*html*/` -
-
+ computed:{ + searchTypesPlaceholder() { + if (!this.searchsettings.types.length) { + return Object.values(this.typeLabels).join(' / '); + } + return this.searchsettings.types.map(type => this.typeLabels[type]).join(' / '); + }, + types() { + if (!this.searchoptions.types) + return []; + if (Array.isArray(this.searchoptions.types)) + return this.searchoptions.types; + return Object.keys(this.searchoptions.types); + }, + typeLabels() { + if (!this.searchoptions.types) + return {}; + if (Array.isArray(this.searchoptions.types)) { + return this.searchoptions.types.reduce((res, type) => { + res[type] = type; + return res + }, {}); + } + return this.searchoptions.types; + } + }, + template: /*html*/` + + -
-
+
-
{{ this.error }}
-
Es wurden keine Ergebnisse gefunden.
-