Fist Draft

This commit is contained in:
cgfhtw
2024-09-20 14:31:25 +02:00
parent e6eea06900
commit 6c08741450
23 changed files with 2838 additions and 541 deletions
+664
View File
@@ -0,0 +1,664 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
// TODO(chris): permissions
// TODO(chris): foto
$config['person'] = [
'primarykey' => 'person_id',
'table' => 'public.tbl_person',
'searchfields' => [
'uid' => [
'comparison' => 'equals',
'field' => 'uid',
'join' => [
'table' => "public.tbl_benutzer",
'using' => "person_id"
],
'1-n' => true
],
'vorname' => [
'alias' => ['firstname'],
'comparison' => 'similar',
'field' => 'vorname'
],
'nachname' => [
'alias' => ['lastname', 'surename'],
'comparison' => 'similar',
'field' => 'nachname'
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)"
],
'email' => [
'comparison' => 'similar',
'field' => 'kontakt',
'join' => [
'table' => "public.tbl_kontakt",
'on' => "kontakttyp = 'email' AND tbl_kontakt.person_id = tbl_person.person_id"
],
"1-n" => true
],
'tel' => [
'alias' => ['phone', 'telefon'],
'comparison' => 'similar',
'field' => 'kontakt',
'join' => [
'table' => "public.tbl_kontakt",
'on' => "kontakttyp IN ('telefon', 'so.tel', 'mobil') AND tbl_kontakt.person_id = tbl_person.person_id"
],
"1-n" => true
],
'preid' => [
'alias' => ['prestudent_id'],
'comparison' => 'equal-int',
'field' => 'prestudent_id',
'join' => [
'table' => "public.tbl_prestudent",
'using' => "person_id"
],
'1-n' => true
],
'pid' => [
'alias' => ['person_id'],
'comparison' => 'equal-int',
'field' => 'person_id'
]
],
'resultfields' => [
"b.uid",
"p.person_id",
"(p.vorname || ' ' || p.nachname) AS name",
"ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email",
"p.foto"
],
'resultjoin' => "
JOIN public.tbl_person p USING (person_id)
LEFT JOIN public.tbl_benutzer b USING (person_id)"
];
$config['student'] = [
'primarykey' => 'student_uid',
'table' => 'public.tbl_student',
'searchfields' => [
'uid' => [
'comparison' => 'equals',
'field' => 'student_uid'
],
'vorname' => [
'alias' => ['firstname'],
'comparison' => 'similar',
'field' => 'vorname',
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'nachname' => [
'alias' => ['lastname', 'surename'],
'comparison' => 'similar',
'field' => 'nachname',
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'email' => [
'comparison' => 'similar',
'field' => 'kontakt',
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_kontakt",
'on' => "kontakttyp = 'email' AND tbl_kontakt.person_id = tbl_prestudent.person_id"
]
],
"1-n" => true
],
'tel' => [
'alias' => ['phone', 'telefon'],
'comparison' => 'similar',
'field' => 'kontakt',
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_kontakt",
'on' => "kontakttyp IN ('telefon', 'so.tel', 'mobil') AND tbl_kontakt.person_id = tbl_prestudent.person_id"
]
],
"1-n" => true
],
'stg' => [
'alias' => ['studiengang'],
'comparison' => 'equals',
'field' => "typ || kurzbz",
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_studiengang",
'on' => "tbl_studiengang.studiengang_kz = tbl_prestudent.studiengang_kz"
]
]
],
'preid' => [
'alias' => ['prestudent_id'],
'comparison' => 'equal-int',
'field' => 'prestudent_id'
],
'pid' => [
'alias' => ['person_id'],
'comparison' => 'equal-int',
'field' => 'person_id',
'join' => [
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
]
]
],
'resultfields' => [
"s.student_uid AS uid",
"s.matrikelnr",
"p.person_id",
"(p.vorname || ' ' || p.nachname) AS name",
"ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email",
"p.foto"
],
'resultjoin' => "
JOIN public.tbl_student s USING (student_uid)
JOIN public.tbl_benutzer b ON(b.uid = s.student_uid)
JOIN public.tbl_person p USING(person_id)"
];
// TODO(chris): "ref"
$config['prestudent'] = [
'primarykey' => 'prestudent_id',
'table' => 'public.tbl_prestudent',
'searchfields' => [
'uid' => [
'comparison' => 'equals',
'field' => 'student_uid',
'join' => [
'table' => "public.tbl_student",
'using' => "prestudent_id"
]
],
'vorname' => [
'alias' => ['firstname'],
'comparison' => 'similar',
'field' => 'vorname',
'join' => [
'table' => "public.tbl_person",
'using' => "person_id"
]
],
'nachname' => [
'alias' => ['lastname', 'surename'],
'comparison' => 'similar',
'field' => 'nachname',
'join' => [
'table' => "public.tbl_person",
'using' => "person_id"
]
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
'join' => [
'table' => "public.tbl_person",
'using' => "person_id"
]
],
'email' => [
'comparison' => 'similar',
'field' => 'kontakt',
'join' => [
'table' => "public.tbl_kontakt",
'on' => "kontakttyp = 'email' AND tbl_kontakt.person_id = tbl_prestudent.person_id"
],
"1-n" => true
],
'tel' => [
'alias' => ['phone', 'telefon'],
'comparison' => 'similar',
'field' => 'kontakt',
'join' => [
'table' => "public.tbl_kontakt",
'on' => "kontakttyp IN ('telefon', 'so.tel', 'mobil') AND tbl_kontakt.person_id = tbl_prestudent.person_id"
],
"1-n" => true
],
'stg' => [
'alias' => ['studiengang'],
'comparison' => 'equals',
'field' => "typ || kurzbz",
'join' => [
'table' => "public.tbl_studiengang",
'using' => "studiengang_kz"
]
],
'preid' => [
'alias' => ['prestudent_id'],
'comparison' => 'equal-int',
'field' => 'prestudent_id'
],
'pid' => [
'alias' => ['person_id'],
'comparison' => 'equal-int',
'field' => 'person_id',
'join' => [
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'resultfields' => [
"ps.prestudent_id",
"ps.studiengang_kz",
"s.matrikelnr",
"p.person_id",
"b.uid",
"(p.vorname || ' ' || p.nachname) AS name",
"ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email",
"p.foto",
"UPPER(sg.typ || sg.kurzbz) AS stg_kuerzel",
"sg.bezeichnung",
"(
SELECT bezeichnung_mehrsprachig[(TABLE lang)]
FROM public.tbl_status
WHERE status_kurzbz = public.get_rolle_prestudent(ps.prestudent_id, NULL)
LIMIT 1
) as status"
],
'resultjoin' => "
LEFT JOIN public.tbl_prestudent ps USING (prestudent_id)
LEFT JOIN public.tbl_student s ON (ps.prestudent_id = s.prestudent_id)
LEFT JOIN public.tbl_benutzer b ON (b.uid = s.student_uid)
JOIN public.tbl_person p ON (p.person_id = ps.person_id)
LEFT JOIN public.tbl_studiengang sg ON (sg.studiengang_kz = ps.studiengang_kz)"
];
$config['employee'] = [
'alias' => ['ma', 'mitarbeiter'],
'primarykey' => 'mitarbeiter_uid',
'table' => 'public.tbl_mitarbeiter',
'searchfields' => [
'uid' => [
'alias' => ['mitarbeiter_uid'],
'comparison' => 'equals',
'field' => "mitarbeiter_uid"
],
'vorname' => [
'alias' => ['firstname'],
'comparison' => 'similar',
'field' => "vorname",
'join' => [
[
'table' => "public.tbl_benutzer",
'on' => "uid = mitarbeiter_uid"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'nachname' => [
'alias' => ['lastname', 'surename'],
'comparison' => 'similar',
'field' => "nachname",
'join' => [
[
'table' => "public.tbl_benutzer",
'on' => "uid = mitarbeiter_uid"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
'join' => [
[
'table' => "public.tbl_benutzer",
'on' => "uid = mitarbeiter_uid"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'email' => [
'comparison' => 'similar',
'field' => "COALESCE(alias, uid) || '" . '@' . DOMAIN . "'",
'join' => [
'table' => "public.tbl_benutzer",
'on' => "uid = mitarbeiter_uid"
]
],
'tel' => [
'alias' => ['phone', 'telefon'],
'comparison' => 'similar',
'field' => "TRIM(COALESCE(kontakt, '') || ' ' || COALESCE(telefonklappe, ''))",
'join' => [
'table' => "public.tbl_kontakt",
'on' => "kontakttyp = 'telefon' AND tbl_kontakt.standort_id = tbl_mitarbeiter.standort_id"
],
"1-n" => true
],
'pid' => [
'alias' => ['person_id'],
'comparison' => 'equal-int',
'field' => "person_id"
],
'oe' => [
'alias' => ['ou', 'organisationseinheit', 'organisationunit'],
'comparison' => 'vector',
'field' => "fts_bezeichnung",
'join' => [
[
'table' => "public.tbl_benutzerfunktion",
'on' => "mitarbeiter_uid = uid
AND funktion_kurzbz = 'oezuordnung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())"
],
[
'table' => "public.tbl_organisationseinheit",
'using' => "oe_kurzbz"
]
],
'1-n' => true
],
'kst' => [
'comparison' => 'vector',
'field' => "fts_bezeichnung",
'join' => [
[
'table' => "public.tbl_benutzerfunktion",
'on' => "mitarbeiter_uid = uid
AND funktion_kurzbz = 'kstzuordnung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())"
],
[
'table' => "public.tbl_organisationseinheit",
'using' => "oe_kurzbz"
]
],
'1-n' => true
]
],
'resultfields' => [
"b.uid",
"p.person_id",
"(p.vorname || ' ' || p.nachname) AS name",
"ARRAY(
SELECT
'[' || ot.bezeichnung || '] ' || o.bezeichnung AS bezeichnung
FROM public.tbl_benutzerfunktion bf
JOIN public.tbl_organisationseinheit o USING(oe_kurzbz)
JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz)
WHERE bf.funktion_kurzbz = 'oezuordnung'
AND (bf.datum_von IS NULL OR bf.datum_von <= NOW())
AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW())
AND bf.uid = b.uid
GROUP BY o.bezeichnung, ot.bezeichnung
) AS organisationunit_name",
"COALESCE(b.alias, b.uid) || '" . '@' . DOMAIN . "' AS email",
"TRIM(COALESCE(k.kontakt, '') || ' ' || COALESCE(m.telefonklappe, '')) AS phone",
"'" . base_url("/cis/public/bild.php?src=person&person_id=") . "' || p.person_id AS photo_url",
"ARRAY(
SELECT
'[' || ot.bezeichnung || '] ' || o.bezeichnung AS bezeichnung
FROM public.tbl_benutzerfunktion bf
JOIN public.tbl_organisationseinheit o USING(oe_kurzbz)
JOIN public.tbl_organisationseinheittyp ot USING(organisationseinheittyp_kurzbz)
WHERE bf.funktion_kurzbz = 'kstzuordnung'
AND (bf.datum_von IS NULL OR bf.datum_von <= NOW())
AND (bf.datum_bis IS NULL OR bf.datum_bis >= NOW())
AND bf.uid = b.uid
GROUP BY o.bezeichnung, ot.bezeichnung
) AS standardkostenstelle"
],
'resultjoin' => "
JOIN public.tbl_mitarbeiter m USING (mitarbeiter_uid)
JOIN public.tbl_benutzer b ON (b.uid = m.mitarbeiter_uid)
JOIN public.tbl_person p USING(person_id)
LEFT JOIN (
SELECT kontakt, standort_id
FROM public.tbl_kontakt
WHERE kontakttyp = 'telefon'
) k ON (k.standort_id = m.standort_id)"
];
$config['unassigned_employee'] = $config['employee'];
$config['unassigned_employee']['alias'] = ['mitarbeiter_ohne_zuordnung'];
$config['unassigned_employee']['prepare'] = "unassigned_employee AS (
SELECT tbl_mitarbeiter.*
FROM public.tbl_mitarbeiter
LEFT JOIN public.tbl_benutzerfunktion ON (
uid = mitarbeiter_uid
AND funktion_kurzbz = 'kstzuordnung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
)
WHERE tbl_benutzerfunktion.bezeichnung IS NULL
UNION
SELECT tbl_mitarbeiter.*
FROM public.tbl_mitarbeiter
LEFT JOIN public.tbl_benutzerfunktion ON (
uid = mitarbeiter_uid
AND funktion_kurzbz = 'oezuordnung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
)
WHERE tbl_benutzerfunktion.bezeichnung IS NULL
)";
$config['unassigned_employee']['table'] = "unassigned_employee";
$config['unassigned_employee']['searchfields']['tel']['join']['on'] = "kontakttyp = 'telefon' AND tbl_kontakt.standort_id = unassigned_employee.standort_id";
$config['organisationunit'] = [
'alias' => ['ou', 'organisationseinheit', 'oe'],
'primarykey' => 'oe_kurzbz',
'table' => 'public.tbl_organisationseinheit',
'searchfields' => [
'uid' => [
'comparison' => 'equals',
'field' => 'uid',
'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS (
SELECT oe_kurzbz, vorname, nachname, uid
FROM public.tbl_benutzerfunktion
JOIN public.tbl_benutzer USING (uid)
JOIN public.tbl_person USING (person_id)
WHERE funktion_kurzbz = 'Leitung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
AND tbl_benutzer.aktiv = TRUE
)",
'join' => [
'table' => "organisationunit_leader",
'using' => "oe_kurzbz"
],
'1-n' => true
],
'vorname' => [
'alias' => ['firstname'],
'comparison' => 'similar',
'field' => 'vorname',
'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS (
SELECT oe_kurzbz, vorname, nachname, uid
FROM public.tbl_benutzerfunktion
JOIN public.tbl_benutzer USING (uid)
JOIN public.tbl_person USING (person_id)
WHERE funktion_kurzbz = 'Leitung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
AND tbl_benutzer.aktiv = TRUE
)",
'join' => [
'table' => "organisationunit_leader",
'using' => "oe_kurzbz"
],
'1-n' => true
],
'nachname' => [
'alias' => ['lastname', 'surename'],
'comparison' => 'similar',
'field' => 'nachname',
'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS (
SELECT oe_kurzbz, vorname, nachname, uid
FROM public.tbl_benutzerfunktion
JOIN public.tbl_benutzer USING (uid)
JOIN public.tbl_person USING (person_id)
WHERE funktion_kurzbz = 'Leitung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
AND tbl_benutzer.aktiv = TRUE
)",
'join' => [
'table' => "organisationunit_leader",
'using' => "oe_kurzbz"
],
'1-n' => true
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
'prepare' => "organisationunit_leader(oe_kurzbz, uid, vorname, nachname) AS (
SELECT oe_kurzbz, vorname, nachname, uid
FROM public.tbl_benutzerfunktion
JOIN public.tbl_benutzer USING (uid)
JOIN public.tbl_person USING (person_id)
WHERE funktion_kurzbz = 'Leitung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
AND tbl_benutzer.aktiv = TRUE
)",
'join' => [
'table' => "organisationunit_leader",
'using' => "oe_kurzbz"
],
'1-n' => true
],
'oe' => [
'alias' => ['ou', 'organisationseinheit', 'organisationunit'],
'comparison' => 'vector',
'field' => "fts_bezeichnung"
],
'kurzbz' => [
'alias' => ['oe_kurzbz'],
'comparison' => 'equals',
'field' => "oe_kurzbz"
]
],
'resultfields' => [
"oe.oe_kurzbz",
"('[' || type.bezeichnung || '] ' || oe.bezeichnung) AS name",
"oe_parent.oe_kurzbz AS parentoe_kurzbz",
"(CASE WHEN oe_parent.bezeichnung IS NOT NULL THEN '[' || type_parent.bezeichnung || '] ' || oe_parent.bezeichnung END) AS parentoe_name",
"ARRAY(
SELECT JSON_BUILD_OBJECT('uid', b.uid, 'vorname', p.vorname, 'nachname', p.nachname, 'name', (p.vorname || ' ' || p.nachname))
FROM public.tbl_benutzerfunktion bf
JOIN public.tbl_benutzer b USING (uid)
JOIN public.tbl_person p USING (person_id)
WHERE funktion_kurzbz = 'Leitung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
AND b.aktiv = TRUE
AND oe_kurzbz = oe.oe_kurzbz
) AS leaders",
"(
SELECT COUNT(*)
FROM public.tbl_benutzerfunktion
WHERE funktion_kurzbz = 'oezuordnung'
AND (datum_von IS NULL OR datum_von <= NOW())
AND (datum_bis IS NULL OR datum_bis >= NOW())
AND oe_kurzbz = oe.oe_kurzbz
) AS number_of_people",
"(CASE WHEN oe.mailverteiler THEN oe.oe_kurzbz || '" . '@' . DOMAIN . "' END) AS mailgroup"
],
'resultjoin' => "
JOIN public.tbl_organisationseinheit oe
USING (oe_kurzbz)
JOIN public.tbl_organisationseinheittyp type
USING (organisationseinheittyp_kurzbz)
LEFT JOIN public.tbl_organisationseinheit oe_parent
ON (oe_parent.oe_kurzbz = oe.oe_parent_kurzbz)
LEFT JOIN public.tbl_organisationseinheittyp type_parent
ON (oe_parent.organisationseinheittyp_kurzbz = type_parent.organisationseinheittyp_kurzbz)"
];
$config['room'] = [
'alias' => ['raum'],
'primarykey' => 'ort_kurzbz',
'table' => 'public.tbl_ort',
'searchfields' => [
'name' => [
'comparison' => 'similar',
'field' => 'ort_kurzbz'
]
],
'resultfields' => [
"ort.ort_kurzbz",
"ort.gebteil AS building",
"ort.ausstattung AS equipment",
"ort.stockwerk AS floor",
"ort.dislozierung AS room_number",
"ort.content_id",
"address.ort AS city",
"address.plz AS zip",
"address.strasse AS street",
"ort.max_person",
"ort.arbeitsplaetze AS workplaces"
],
'resultjoin' => "
JOIN public.tbl_ort ort
USING (ort_kurzbz)
LEFT JOIN public.tbl_standort
USING (standort_id)
LEFT JOIN public.tbl_adresse address
USING (adresse_id)"
];
+31
View File
@@ -0,0 +1,31 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
$config['equal-int'] = [
'priority' => 4,
'rank' => "0",
'compare' => "{field} = {word}",
'force_integer' => true
];
$config['equals'] = [
'priority' => 3,
'rank' => "0",
'compare' => "LOWER({field}) = {word}"
];
$config['similar'] = [
'priority' => 2,
'rank' => "(COALESCE({field}, '') <->> {word})",
'compare' => "COALESCE({field}, '') %> {word}",
'compare_boolean' => "COALESCE({field}, '') ILIKE {like:word}"
];
$config['vector'] = [
'priority' => 1,
'rank' => "ts_rank_cd({field}, to_tsquery('simple', {word}))",
'compare' => "to_tsquery('simple', {word}) @@ {field}"
];
@@ -50,6 +50,7 @@ class Searchbar extends FHCAPI_Controller
*/
public function search()
{
#$searchstrings = ['pid:71995', 'email:eder.iris', 'schnabl', 'schnabl thomas', 'schnabl thomas sarim', 'schnabl -thomas', 'nachname:schnabl -vorname:thomas', 'schnabl thomas -sarim', 'schnabl or hacker', '-ali', '-ali -baba', '-ali -baba -raub', '-ali -honig', '-ali -baba ali', 'hofer martin'];
$this->load->library('form_validation');
// Checks if the searchstr and the types parameters are in the POSTed JSON
@@ -57,13 +58,17 @@ class Searchbar extends FHCAPI_Controller
$this->form_validation->set_rules(self::TYPES_PARAM . '[]', null, 'required');
if (!$this->form_validation->run())
$this->terminateWithError(SearchBarLib::ERROR_WRONG_JSON, self::ERROR_TYPE_GENERAL);
$this->terminateWithValidationErrors($this->form_validation->error_array());
// Convert to json the result from searchbarlib->search
$result = $this->searchbarlib->search($this->input->post(self::SEARCHSTR_PARAM), $this->input->post(self::TYPES_PARAM));
if (property_exists($result, 'error'))
$this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
$this->terminateWithSuccess($result);
$data = $this->getDataOrTerminateWithError($result);
$this->addMeta('time', $result->meta['time']);
$this->addMeta('searchstring', $result->meta['searchstring']);
$this->terminateWithSuccess($data);
}
}
+7 -2
View File
@@ -106,10 +106,15 @@ class FHCAPI_Controller extends Auth_Controller
$error = [];
if (is_array($data)) {
if ($type == self::ERROR_TYPE_VALIDATION)
if ($type == self::ERROR_TYPE_VALIDATION) {
$error['messages'] = $data;
else
} elseif (array_is_list($data)) {
foreach ($data as $d)
$this->addError($d, $type);
return;
} else {
$error = $data;
}
} elseif (is_object($data)) {
$error = (array)$data;
} else {
+17
View File
@@ -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
// ------------------------------------------------------------------------
File diff suppressed because it is too large Load Diff
+38
View File
@@ -92,4 +92,42 @@
.searchbar_inline_ul li {
list-style: none;
}
/* new variant with template/frame */
.searchbar-result {
border-bottom: 1px solid lightgrey;
margin-bottom: 1rem;
padding-bottom: 1rem;
}
.searchbar-actions {
display: flex;
flex-wrap: wrap;
gap: .5rem;
padding: 0;
margin: 1rem 0 0;
}
.searchbar-square-image {
position: relative;
display: block;
height: 0;
padding-bottom: 100%;
}
.searchbar-square-image > * {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.searchbar-square-image img {
object-fit: cover;
}
.no-margin-paragraphs p {
margin: 0;
}
+2 -2
View File
@@ -16,9 +16,9 @@
*/
export default {
search(searchsettings) {
search(searchsettings, config) {
const url = '/api/frontend/v1/searchbar/search';
return this.$fhcApi.post(url, searchsettings);
return this.$fhcApi.post(url, searchsettings, config);
},
searchdummy(searchsettings) {
const url = 'public/js/apps/api/dummyapi.php/Search';
@@ -0,0 +1,59 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
template: `
<template-frame
class="searchbar-result-student"
:res="res"
:actions="actions"
:title="res.name"
:image="res.photo_url"
image-fallback="fas fa-user-circle fa-7x"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Standard-Kostenstelle</div>
<div class="searchbar_tablecell">
<ul class="searchbar_inline_ul" v-if="res.standardkostenstelle.length > 0">
<li v-for="(stdkst, idx) in res.standardkostenstelle" :key="idx">{{ stdkst }}</li>
</ul>
<span v-else="">keine</span>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Organisations-Einheit</div>
<div class="searchbar_tablecell">
<ul class="searchbar_inline_ul" v-if="res.organisationunit_name.length > 0">
<li v-for="(oe, idx) in res.organisationunit_name" :key="idx">{{ oe }}</li>
</ul>
<span v-else="">keine</span>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMails</div>
<div class="searchbar_tablecell">
<a :href="'mailto:' + res.email" class="d-block">
{{ res.email }}
</a>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Telefon</div>
<div class="searchbar_tablecell">
<a :href="'tel:' + res.phone" class="d-block">
{{ res.phone }}
</a>
</div>
</div>
</div>
</template-frame>`
};
@@ -0,0 +1,166 @@
import TemplateFrame from "./template/frame.js";
import TemplateAction from "./template/action.js";
export default {
components: {
TemplateFrame,
TemplateAction
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
telurl() {
return 'tel:' + this.employee?.phone;
},
person() {
const person = this.res.list.filter(item => item.type == 'person');
if (person.length)
return person.pop();
const { person_id, name, foto, photo_url, email } = this.res.list[0];
return { person_id, name, foto, photo_url, email };
},
employee() {
const ma = this.res.list.filter(item => [
'employee',
'unassigned_employee'
].includes(item.type));
return ma.length ? ma.pop() : null;
},
students() {
const students = this.res.list.filter(item => item.type == 'prestudent');
return students.length ? students : null;
},
foto() {
if (this.person.foto)
return 'data:image/jpeg;base64,' + this.person.foto;
return this.person.photo_url;
},
emails() {
if (Array.isArray(this.person.email))
return this.person.email;
return [this.person.email];
}
},
template: `
<template-frame
class="searchbar-result-mergedperson"
:res="person"
:actions="actions"
:title="person.name"
:image="foto"
image-fallback="fas fa-user-circle fa-7x"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Person ID</div>
<div class="searchbar_tablecell">
{{ person.person_id }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMails</div>
<div class="searchbar_tablecell">
<a v-for="email in emails" :key="email" :href="'mailto:' + email" class="d-block">
{{ email }}
</a>
</div>
</div>
<template v-if="employee">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">
<template-action
:res="employee"
:action="actions.defaultactionemployee || actions.defaultaction"
@actionexecuted="$emit('actionexecuted')"
>
Mitarbeiter
</template-action>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Standard-Kostenstelle</div>
<div class="searchbar_tablecell">
<ul class="searchbar_inline_ul" v-if="employee.standardkostenstelle.length > 0">
<li
v-for="(stdkst, idx) in employee.standardkostenstelle"
:key="idx"
>
{{ stdkst }}
</li>
</ul>
<span v-else="">keine</span>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Organisations-Einheit</div>
<div class="searchbar_tablecell">
<ul class="searchbar_inline_ul" v-if="employee.organisationunit_name.length > 0">
<li
v-for="(oe, idx) in employee.organisationunit_name"
:key="idx"
>
{{ oe }}
</li>
</ul>
<span v-else="">keine</span>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Telefon</div>
<div class="searchbar_tablecell">
<a :href="telurl">
{{ employee.phone }}
</a>
</div>
</div>
</template>
<template v-if="students">
<template v-for="student in students">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">
<template-action
v-if="actions.defaultaction"
:res="student"
:action="actions.defaultactionstudent || actions.defaultaction"
@actionexecuted="$emit('actionexecuted')"
>
{{ student.status }} ({{ student.stg_kuerzel }})
</template-action>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Studiengang</div>
<div class="searchbar_tablecell">
{{ student.bezeichnung }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Prestudent ID</div>
<div class="searchbar_tablecell">
{{ student.prestudent_id }}
</div>
</div>
<div v-if="student.uid" class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Student UID</div>
<div class="searchbar_tablecell">
{{ student.uid }}
</div>
</div>
<div v-if="student.matrikelnr" class="searchbar_tablerow">
<div class="searchbar_tablecell ps-3">Matrikelnummer</div>
<div class="searchbar_tablecell">
{{ student.matrikelnr }}
</div>
</div>
</template>
</template>
</div>
</template-frame>`
};
@@ -0,0 +1,35 @@
import ResultPrestudent from "./prestudent.js";
import ResultStudent from "./student.js";
export default {
components: {
ResultPrestudent,
ResultStudent
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
prestudent() {
const prestudent = this.res.list.filter(item => item.type == 'prestudent');
return prestudent.pop();
}
},
template: `
<result-prestudent
v-if="prestudent"
:res="prestudent"
:actions="actions"
@actionexecuted="$emit('actionexecuted')"
class="searchbar-result-mergedstudent"
></result-prestudent>
<result-student
v-else
:res="res.list[0]"
:actions="actions"
@actionexecuted="$emit('actionexecuted')"
class="searchbar-result-mergedstudent"
></result-student>`
};
@@ -0,0 +1,63 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
foto() {
if (this.res.foto)
return 'data:image/jpeg;base64,' + this.res.foto;
return null;
}
},
template: `
<template-frame
class="searchbar-result-organisationunit"
:res="res"
:actions="actions"
:title="res.name"
image-fallback="fas fa-sitemap fa-4x p-4 text-white bg-primary"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">übergeordnete OrgEinheit</div>
<div class="searchbar_tablecell">
{{ res.parentoe_name }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Gruppen-EMail</div>
<div class="searchbar_tablecell">
<a :href="'mailto:' + res.mailgroup">
{{ res.mailgroup }}
</a>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Leiter</div>
<div class="searchbar_tablecell">
<ul class="searchbar_inline_ul" v-if="res.leaders.length > 0">
<li v-for="(leader, idx) in res.leaders" :key="idx">{{ leader.name }}</li>
</ul>
<span v-else="">N.N.</span>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Mitarbeiter-Anzahl</div>
<div class="searchbar_tablecell">
{{ res.number_of_people }}
</div>
</div>
</div>
</template-frame>`
};
@@ -0,0 +1,46 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
foto() {
if (this.res.foto)
return 'data:image/jpeg;base64,' + this.res.foto;
return null;
}
},
template: `
<template-frame
class="searchbar-result-student"
:res="res"
:actions="actions"
:title="res.name"
:image="foto"
image-fallback="fas fa-user-circle fa-7x"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Person ID</div>
<div class="searchbar_tablecell">
{{ res.person_id }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMails</div>
<div class="searchbar_tablecell">
<a v-for="email in res.email" :key="email" :href="'mailto:' + email" class="d-block">
{{ email }}
</a>
</div>
</div>
</div>
</template-frame>`
};
@@ -0,0 +1,70 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
foto() {
if (this.res.foto)
return 'data:image/jpeg;base64,' + this.res.foto;
return null;
}
},
template: `
<template-frame
class="searchbar-result-prestudent"
:res="res"
:actions="actions"
:title="res.name + ' (' + res.status + ' ' + res.stg_kuerzel + ')'"
:image="foto"
image-fallback="fas fa-user-circle fa-7x"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Person ID</div>
<div class="searchbar_tablecell">
{{ res.person_id }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMails</div>
<div class="searchbar_tablecell">
<a v-for="email in res.email" :key="email" :href="'mailto:' + email" class="d-block">
{{ email }}
</a>
</div>
</div>
<div v-if="res.uid" class="searchbar_tablerow">
<div class="searchbar_tablecell">Student UID</div>
<div class="searchbar_tablecell">
{{ res.uid }}
</div>
</div>
<div v-if="res.matrikelnr" class="searchbar_tablerow">
<div class="searchbar_tablecell">Matrikelnummer</div>
<div class="searchbar_tablecell">
{{ res.matrikelnr }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Prestudent ID</div>
<div class="searchbar_tablecell">
{{ res.prestudent_id }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Studiengang</div>
<div class="searchbar_tablecell">
{{ res.bezeichnung }}
</div>
</div>
</div>
</template-frame>`
};
@@ -0,0 +1,74 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
equipment() {
if (!this.res.equipment)
return "";
return this.res.equipment.replace(new RegExp('<br />', 'ig'), '');
},
address() {
let address = this.res.zip || '';
if (this.res.city)
address += (address ? ' ' : '') + this.res.city;
if (this.res.street)
address += (address ? ', ' : '') + this.res.street;
if (this.res.floor)
address += (address ? ' / ' : '') + this.res.floor + ' Stockwerk';
return address || 'N/A';
}
},
template: `
<template-frame
class="searchbar-result-room"
:res="res"
:actions="actions"
:title="res.ort_kurzbz"
image-fallback="fas fa-door-open fa-4x p-4 text-white bg-primary"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Standort</div>
<div class="searchbar_tablecell">
{{ address }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Sitzplätze</div>
<div class="searchbar_tablecell">
<template v-if="res.max_person !== null && res.workplaces !== null">
{{ res.max_person }}, davon {{ res.workplaces }} PC-Plätze
</template>
<template v-else>
N/A
</template>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Gebäude</div>
<div class="searchbar_tablecell">
{{ res.building }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Zusatz Informationen</div>
<div class="searchbar_tablecell">
<div class="no-margin-paragraphs" v-html="equipment"></div>
</div>
</div>
</div>
</template-frame>`
};
@@ -0,0 +1,58 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
computed: {
foto() {
if (this.res.foto)
return 'data:image/jpeg;base64,' + this.res.foto;
return null;
}
},
template: `
<template-frame
class="searchbar-result-student"
:res="res"
:actions="actions"
:title="res.name"
:image="foto"
image-fallback="fas fa-user-circle fa-7x"
@actionexecuted="$emit('actionexecuted')"
>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Student UID</div>
<div class="searchbar_tablecell">
{{ res.uid }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Person ID</div>
<div class="searchbar_tablecell">
{{ res.person_id }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Matrikelnummer</div>
<div class="searchbar_tablecell">
{{ res.matrikelnr }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMails</div>
<div class="searchbar_tablecell">
<a v-for="email in res.email" :key="email" :href="'mailto:' + email" class="d-block">
{{ email }}
</a>
</div>
</div>
</div>
</template-frame>`
};
@@ -0,0 +1,28 @@
export default {
emits: [ 'actionexecuted' ],
props: {
res: Object,
action: Object
},
computed: {
actionHref() {
if (this.action.type !== 'link')
return 'javascript:void(0);';
return typeof this.action.action === 'function'
? this.action.action(this.res)
: this.action.action;
}
},
methods: {
actionFunc() {
if (this.action.type !== 'function')
return;
this.action.action(this.res);
this.$emit('actionexecuted');
}
},
template: `
<a :href="actionHref" @click="actionFunc">
<slot>Action</slot>
</a>`
};
@@ -0,0 +1,26 @@
import ResultAction from "./action.js";
export default {
components: {
ResultAction
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Array
},
template: `
<div v-if="actions.length" class="searchbar-actions">
<result-action
v-for="(action, index) in actions"
:key="action.label"
:res="res"
:action="action"
class="btn btn-primary btn-sm"
@actionexecuted="$emit('actionexecuted')"
>
<i v-if="action.icon" :class="action.icon"></i>
<span class="p-2">{{ action.label }}</span>
</result-action>
</div>`
};
@@ -0,0 +1,59 @@
import ResultAction from "./action.js";
import ResultActions from "./actions.js";
export default {
components: {
ResultAction,
ResultActions
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object,
title: String,
image: String,
imageFallback: String
},
template: `
<div class="searchbar-result">
<div class="searchbar_grid">
<div class="searchbar_icon">
<result-action
:res="res"
:action="actions.defaultaction"
@actionexecuted="$emit('actionexecuted')"
class="searchbar-square-image"
>
<img
v-if="image"
:src="image"
class="rounded-circle"
/>
<div v-else class="d-flex justify-content-center align-items-center rounded-circle overflow-hidden">
<i :class="imageFallback"></i>
</div>
</result-action>
</div>
<div class="searchbar_data">
<result-action
:res="res"
:action="actions.defaultaction"
@actionexecuted="$emit('actionexecuted')"
class="mb-3"
>
<span class="fw-bold">{{ title }}</span>
</result-action>
<slot></slot>
<result-actions
:res="res"
:actions="actions.childactions"
@actionexecuted="$emit('actionexecuted')"
>
</result-actions>
</div>
</div>
</div>`
};
+276 -162
View File
@@ -1,168 +1,282 @@
import person from "./person.js";
import raum from "./raum.js";
import employee from "./employee.js";
import organisationunit from "./organisationunit.js";
import student from "./student.js";
import prestudent from "./prestudent.js";
import ResultPerson from "./result/person.js";
import ResultStudent from "./result/student.js";
import ResultPrestudent from "./result/prestudent.js";
import ResultEmployee from "./result/employee.js";
import ResultOrganisationunit from "./result/organisationunit.js";
import ResultRoom from "./result/room.js";
import ResultMergedperson from "./result/mergedperson.js";
import ResultMergedstudent from "./result/mergedstudent.js";
// TODO(chris): arrays in results
export default {
props: [ "searchoptions", "searchfunction" ],
data: function() {
return {
searchtimer: null,
hidetimer: null,
showsettings: false,
searchsettings: {
searchstr: '',
types: []
},
showresult: false,
searchresult: [],
searching: false,
error: null
};
},
components: {
person: person,
raum: raum,
employee: employee,
organisationunit: organisationunit,
student: student,
prestudent: prestudent
},
template: `
<form ref="searchform" class="d-flex me-3 position-relative" action="javascript:void(0);"
@focusin="this.searchfocusin" @focusout="this.searchfocusout">
<div class="input-group me-2 bg-white">
<input ref="searchbox" @keyup="this.search" @focus="this.showsearchresult"
v-model="this.searchsettings.searchstr" class="form-control"
type="search" placeholder="Suche..." aria-label="Search">
<button ref="settingsbutton" @click="this.togglesettings" class="btn btn-light border-start" type="button" id="search-filter"><i class="fas fa-cog"></i></button>
</div>
<div v-show="this.showresult" ref="result"
class="searchbar_results" tabindex="-1">
<div v-if="this.searching">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
<div v-else-if="this.error !== null">{{ this.error }}</div>
<div v-else-if="this.searchresult.length < 1">Es wurden keine Ergebnisse gefunden.</div>
<template v-else="" v-for="res in this.searchresult">
<person v-if="res.type === 'person'" :res="res" :actions="this.searchoptions.actions.person" @actionexecuted="this.hideresult"></person>
<student v-else-if="res.type === 'student'" :res="res" :actions="this.searchoptions.actions.student" @actionexecuted="this.hideresult"></student>
<prestudent v-else-if="res.type === 'prestudent'" :res="res" :actions="this.searchoptions.actions.prestudent" @actionexecuted="this.hideresult"></prestudent>
<employee v-else-if="res.type === 'mitarbeiter'" :res="res" :actions="this.searchoptions.actions.employee" @actionexecuted="this.hideresult"></employee>
<employee v-else-if="res.type === 'mitarbeiter_ohne_zuordnung'" :res="res" :actions="this.searchoptions.actions.employee" @actionexecuted="this.hideresult"></employee>
<organisationunit v-else-if="res.type === 'organisationunit'" :res="res" :actions="this.searchoptions.actions.organisationunit" @actionexecuted="this.hideresult"></organisationunit>
<raum v-else-if="res.type === 'raum'" :res="res" :actions="this.searchoptions.actions.raum" @actionexecuted="this.hideresult"></raum>
<div v-else="">Unbekannter Ergebnistyp: '{{ res.type }}'.</div>
</template>
</div>
<div v-show="this.showsettings" ref="settings"
class="searchbar_settings" tabindex="-1">
<div class="btn-group" v-if="this.searchoptions.types.length > 0">
<template v-for="(type, index) in this.searchoptions.types" :key="type">
<input type="checkbox" class="btn-check" :id="this.$.uid + 'search_type_' + index" :value="type" v-model="this.searchsettings.types"/>
<label class="btn btn-outline-secondary" :for="this.$.uid + 'search_type_' + index">{{ type }}</label>
</template>
</div>
<div class="mb-2"></div>
<button ref="settingsrefreshsearch" @click="this.refreshsearch" class="btn btn-primary" type="button">Übernehmen</button>
</div>
</form>
`,
beforeMount: function() {
this.updateSearchOptions();
},
methods: {
updateSearchOptions: function() {
this.searchsettings.types = [];
for( const idx in this.searchoptions.types ) {
this.searchsettings.types.push(this.searchoptions.types[idx]);
}
},
calcSearchResultExtent: function() {
var rect = this.$refs.searchbox.getBoundingClientRect();
components: {
ResultPerson,
ResultStudent,
ResultPrestudent,
ResultEmployee,
ResultOrganisationunit,
ResultRoom,
ResultMergedperson,
ResultMergedstudent
},
props: [ "searchoptions", "searchfunction" ],
data() {
return {
searchtimer: null,
hidetimer: null,
showsettings: false,
searchsettings: {
searchstr: '',
types: []
},
showresult: false,
searchresult: [],
searching: false,
error: null,
abortController: null,
retry: 5
};
},
beforeMount() {
this.updateSearchOptions();
},
methods: {
updateSearchOptions() {
this.searchsettings.types = [];
for (const idx in this.searchoptions.types) {
this.searchsettings.types.push(this.searchoptions.types[idx]);
}
},
calcSearchResultExtent() {
var rect = this.$refs.searchbox.getBoundingClientRect();
//console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect));
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.80) + 'px';
},
search: function() {
if( this.searchtimer !== null ) {
clearTimeout(this.searchtimer);
}
if( this.searchsettings.searchstr.length >= 2 ) {
this.calcSearchResultExtent();
this.searchtimer = setTimeout(
this.callsearchapi,
500
);
} else {
this.showresult = false;
}
},
callsearchapi: function() {
var that = this;
this.error = null;
this.searchresult = [];
this.searching = true;
this.showsearchresult();
this.searchfunction(this.searchsettings)
.then(function(response) {
if( response.data?.error === 1 ) {
that.error = 'Bei der Suche ist ein Fehler aufgetreten.';
} else {
that.searchresult = response.data.data;
}
})
.catch(function(error) {
that.error = 'Bei der Suche ist ein Fehler aufgetreten.'
+ ' ' + error.message;
})
.finally(function() {
that.searching = false;
});
},
refreshsearch: function() {
this.search();
this.togglesettings();
},
calcSearchSettingsExtent: function() {
var rect = this.$refs.settingsbutton.getBoundingClientRect();
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.80) + 'px';
},
search() {
if (this.searchtimer !== null) {
clearTimeout(this.searchtimer);
}
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
if (this.searchsettings.searchstr.length >= 2) {
this.calcSearchResultExtent();
this.searchtimer = setTimeout(
this.callsearchapi,
500
);
} else {
this.showresult = false;
}
},
callsearchapi() {
this.error = null;
this.searchresult = [];
this.searching = true;
this.showsearchresult();
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
this
.searchfunction(this.searchsettings, { signal: this.abortController.signal })
.then(response => {
if (!response.data) {
this.error = 'Bei der Suche ist ein Fehler aufgetreten.';
} else {
let res = response.data.map(el => ({...el, ...JSON.parse(el.data)}));
if (this.searchoptions.mergeResults) {
let counter = 0;
let mergeTypes = [];
let mergedType = 'merged';
let mergeKey = '';
switch (this.searchoptions.mergeResults) {
case 'student':
mergeTypes = ['student', 'prestudent'];
mergedType += this.searchoptions.mergeResults;
mergeKey = 'uid';
break;
case 'person':
mergeTypes = ['person', 'employee', 'unassigned_employee', 'mitarbeiter', 'mitarbeiter_ohne_zuordnung', 'student', 'prestudent'];
mergedType += this.searchoptions.mergeResults;
mergeKey = 'person_id';
break;
}
if (mergeTypes.length) {
res = Object.values(res.reduce((a, c) => {
if (!mergeTypes.includes(c.type)) {
a['nomerge' + counter++] = c;
} else if (c[mergeKey] === null) {
a['nomerge' + counter++] = c;
} else if (a[c[mergeKey]] === undefined) {
a[c[mergeKey]] = {
rank: c.rank,
type: mergedType,
list: [c]
};
} else {
a[c[mergeKey]].list.push(c);
if (c.rank > a[c[mergeKey]].rank)
a[c[mergeKey]].rank = c.rank;
}
return a;
}, {})).sort((a, b) => b.rank - a.rank);
}
}
this.searchresult = res;
}
this.searching = false;
this.retry = 5;
})
.catch(error => {
if (error.code == "ERR_CANCELED") {
return this.retry = 5;
}
if (error.code == "ECONNABORTED" && this.retry) {
this.retry--;
return this.callsearchapi();
}
this.error = 'Bei der Suche ist ein Fehler aufgetreten.' + ' ' + error.message;
this.searching = false;
this.retry = 5;
});
},
refreshsearch() {
this.search();
this.togglesettings();
},
calcSearchSettingsExtent() {
var rect = this.$refs.settingsbutton.getBoundingClientRect();
//console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect));
this.$refs.settings.style.top = Math.floor(rect.bottom + 3) + 'px';
this.$refs.settings.style.right = Math.floor(window.innerWidth - rect.right) + 'px';
this.$refs.settings.style.width = Math.floor(window.innerWidth * 0.5) + 'px';
this.$refs.settings.style.top = Math.floor(rect.bottom + 3) + 'px';
this.$refs.settings.style.right = Math.floor(window.innerWidth - rect.right) + 'px';
this.$refs.settings.style.width = Math.floor(window.innerWidth * 0.5) + 'px';
//this.$refs.settings.style.height = Math.floor(window.innerHeight * 0.5) + 'px';
},
togglesettings: function() {
this.showsettings = !this.showsettings;
this.calcSearchSettingsExtent();
},
hideresult: function() {
this.showresult = false;
window.removeEventListener('resize', this.calcSearchResultExtent);
},
showsearchresult: function() {
if( this.searchsettings.searchstr.length >= 3 ) {
this.showresult = true;
window.addEventListener('resize', this.calcSearchResultExtent);
}
},
searchfocusin: function(e) {
e.preventDefault();
e.stopPropagation();
if( this.hidetimer !== null ) {
clearTimeout(this.hidetimer);
}
},
searchfocusout: function(e) {
e.preventDefault();
e.stopPropagation();
this.hidetimer = setTimeout(
this.hideresult,
100
);
}
}
},
togglesettings() {
this.showsettings = !this.showsettings;
this.calcSearchSettingsExtent();
},
hideresult() {
this.showresult = false;
window.removeEventListener('resize', this.calcSearchResultExtent);
},
showsearchresult() {
if (this.searchsettings.searchstr.length >= 3) {
this.showresult = true;
window.addEventListener('resize', this.calcSearchResultExtent);
}
},
searchfocusin(e) {
e.preventDefault();
e.stopPropagation();
if (this.hidetimer !== null) {
clearTimeout(this.hidetimer);
}
},
searchfocusout(e) {
e.preventDefault();
e.stopPropagation();
this.hidetimer = setTimeout(
this.hideresult,
100
);
}
},
template: `
<form
ref="searchform"
class="d-flex me-3 position-relative"
action="javascript:void(0);"
@focusin="searchfocusin"
@focusout="searchfocusout"
>
<div class="input-group me-2 bg-white">
<input
ref="searchbox"
@input="search"
@focus="showsearchresult"
v-model="searchsettings.searchstr"
class="form-control"
type="search"
placeholder="Suche..."
aria-label="Search"
>
<button
ref="settingsbutton"
@click="togglesettings"
class="btn btn-light border-start"
type="button"
id="search-filter"
>
<i class="fas fa-cog"></i>
</button>
</div>
<div
v-show="showresult"
ref="result"
class="searchbar_results"
tabindex="-1"
>
<div v-if="searching">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
<div v-else-if="error !== null">{{ error }}</div>
<div v-else-if="searchresult.length < 1">Es wurden keine Ergebnisse gefunden.</div>
<template v-else>
<template v-for="res in searchresult">
<result-person v-if="res.type === 'person'" :res="res" :actions="searchoptions.actions.person" @actionexecuted="hideresult"></result-person>
<result-student v-else-if="res.type === 'student'" :res="res" :actions="searchoptions.actions.student" @actionexecuted="hideresult"></result-student>
<result-prestudent v-else-if="res.type === 'prestudent'" :res="res" :actions="searchoptions.actions.prestudent" @actionexecuted="hideresult"></result-prestudent>
<result-employee v-else-if="res.type === 'employee'" :res="res" :actions="searchoptions.actions.employee" @actionexecuted="hideresult"></result-employee>
<result-employee v-else-if="res.type === 'unassigned_employee'" :res="res" :actions="searchoptions.actions.employee" @actionexecuted="hideresult"></result-employee>
<result-organisationunit v-else-if="res.type === 'organisationunit'" :res="res" :actions="searchoptions.actions.organisationunit" @actionexecuted="hideresult"></result-organisationunit>
<result-room v-else-if="res.type === 'room'" :res="res" :actions="searchoptions.actions.room" @actionexecuted="hideresult"></result-room>
<result-mergedperson v-else-if="res.type === 'mergedperson'" :res="res" :actions="searchoptions.actions.mergedperson" @actionexecuted="hideresult"></result-mergedperson>
<result-mergedstudent v-else-if="res.type === 'mergedstudent'" :res="res" :actions="searchoptions.actions.mergedstudent" @actionexecuted="hideresult"></result-mergedstudent>
<div v-else>Unbekannter Ergebnistyp: '{{ res.type }}'.</div>
</template>
</template>
</div>
<div
v-show="showsettings"
ref="settings"
class="searchbar_settings"
tabindex="-1"
>
<div class="btn-group" v-if="searchoptions.types.length > 0">
<template v-for="(type, index) in searchoptions.types" :key="type">
<input
type="checkbox"
class="btn-check"
:id="$.uid + 'search_type_' + index"
:value="type"
v-model="searchsettings.types"
/>
<label
class="btn btn-outline-secondary"
:for="$.uid + 'search_type_' + index"
>
{{ type }}
</label>
</template>
</div>
<div class="mb-2"></div>
<button
ref="settingsrefreshsearch"
@click="refreshsearch"
class="btn btn-primary"
type="button"
>
Übernehmen
</button>
</div>
</form>`
};
+2
View File
@@ -40,6 +40,8 @@ export default {
function _clean_return_value(response) {
const result = response.data;
delete response.data;
if (!result)
return {meta: {response}, data: null};
if (!result.meta)
result.meta = {response};
else
+1
View File
@@ -58,6 +58,7 @@ require_once('dbupdate_3.4/17513_Entwicklungsteam.php');
require_once('dbupdate_3.4/28575_softwarebereitstellung.php');
require_once('dbupdate_3.4/41150_oe-pfad_db_view.php');
require_once('dbupdate_3.4/44031_stv_favorites.php');
require_once('dbupdate_3.4/40128_search.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
+171
View File
@@ -0,0 +1,171 @@
<?php
if (!defined('DB_NAME')) exit('No direct script access allowed');
// Activate module pg_trgm
if (!$db->db_num_rows(@$db->db_query("SELECT 1
FROM pg_extension WHERE extname = 'pg_trgm' LIMIT 1;")))
{
$qry = "CREATE extension pg_trgm;";
if (!$db->db_query($qry))
echo '<strong>Module pg_trgm ' . $db->db_last_error() . '</strong><br>';
else
echo 'Module pg_trgm: activated<br>';
}
// Add additional computed columns
// Add column fts_bezeichnung to public.tbl_organisationseinheit
if (!@$db->db_query("SELECT fts_bezeichnung FROM public.tbl_organisationseinheit LIMIT 1"))
{
$qry = "ALTER TABLE public.tbl_organisationseinheit ADD COLUMN fts_bezeichnung tsvector;";
$qry .= "COMMENT ON COLUMN public.tbl_organisationseinheit.fts_bezeichnung IS 'used for search - auto generated w triggers';";
if (!$db->db_query($qry))
echo '<strong> public.tbl_organisationseinheit ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_organisationseinheit: new column "fts_bezeichnung" added<br>';
}
// Add function tr_update_tbl_organisationseinheit_fts_bezeichnung to public
if (!$db->db_num_rows(@$db->db_query("SELECT 1 FROM pg_proc WHERE proname = 'tr_update_tbl_organisationseinheit_fts_bezeichnung' LIMIT 1;")))
{
$qry = "CREATE FUNCTION tr_update_tbl_organisationseinheit_fts_bezeichnung()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF TG_TABLE_NAME = 'tbl_organisationseinheit' THEN
NEW.fts_bezeichnung := to_tsvector('simple', COALESCE((SELECT bezeichnung FROM public.tbl_organisationseinheittyp WHERE organisationseinheittyp_kurzbz = NEW.organisationseinheittyp_kurzbz), '') || ' ' || COALESCE(NEW.bezeichnung, ''));
ELSIF TG_TABLE_NAME = 'tbl_organisationseinheittyp' THEN
UPDATE public.tbl_organisationseinheit SET fts_bezeichnung = to_tsvector('simple', COALESCE(NEW.bezeichnung, '') || ' ' || COALESCE(bezeichnung, '')) WHERE organisationseinheittyp_kurzbz = NEW.organisationseinheittyp_kurzbz;
END IF;
RETURN NEW;
END;
$$";
if (!$db->db_query($qry))
echo '<strong> public.tr_update_tbl_organisationseinheit_fts_bezeichnung ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tr_update_tbl_organisationseinheit_fts_bezeichnung: function created<br>';
}
$update_column = false;
// Add trigger tr_organisationseinheit_update_organisationseinheittyp_kurzbz to public.tbl_organisationseinheit
if (!$db->db_num_rows(@$db->db_query("SELECT 1 FROM information_schema.triggers WHERE event_object_table ='tbl_organisationseinheit' AND trigger_name = 'tr_organisationseinheit_update_organisationseinheittyp_kurzbz' LIMIT 1;")))
{
$qry = "CREATE TRIGGER tr_organisationseinheit_update_organisationseinheittyp_kurzbz
BEFORE UPDATE OF organisationseinheittyp_kurzbz OR INSERT
ON public.tbl_organisationseinheit
FOR EACH ROW
EXECUTE FUNCTION tr_update_tbl_organisationseinheit_fts_bezeichnung();";
if (!$db->db_query($qry))
echo '<strong> public.tbl_organisationseinheit ' . $db->db_last_error() . '</strong><br>';
else {
echo 'public.tbl_organisationseinheit: trigger "tr_organisationseinheit_update_organisationseinheittyp_kurzbz" created<br>';
$update_column = true;
}
}
// Add trigger tr_organisationseinheittyp_update_bezeichnung to public.tbl_organisationseinheittyp
if (!$db->db_num_rows(@$db->db_query("SELECT 1 FROM information_schema.triggers WHERE event_object_table ='tbl_organisationseinheittyp' AND trigger_name = 'tr_organisationseinheittyp_update_bezeichnung' LIMIT 1;")))
{
$qry = "CREATE TRIGGER tr_organisationseinheittyp_update_bezeichnung
BEFORE UPDATE OF bezeichnung
ON public.tbl_organisationseinheittyp
FOR EACH ROW
EXECUTE FUNCTION tr_update_tbl_organisationseinheit_fts_bezeichnung();";
if (!$db->db_query($qry))
echo '<strong> public.tbl_organisationseinheittyp ' . $db->db_last_error() . '</strong><br>';
else {
echo 'public.tbl_organisationseinheittyp: trigger "tr_organisationseinheittyp_update_bezeichnung" created<br>';
$update_column = true;
}
}
// Update fts_bezeichnung on tbl_organisationseinheit with new triggers
if ($update_column)
{
$qry = "UPDATE public.tbl_organisationseinheittyp SET bezeichnung = bezeichnung;";
if (!$db->db_query($qry))
echo '<strong> public.tbl_organisationseinheit ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_organisationseinheit: column "fts_bezeichnung" updated<br>';
}
// Add Trigram Indexes
// Add index for kontakt to public.tbl_kontakt
if ($result = @$db->db_query("SELECT 1
FROM pg_indexes WHERE indexname = 'idx_tbl_kontakt_kontakt_trgm';"))
{
if ($db->db_num_rows($result) == 0)
{
$qry = "CREATE INDEX idx_tbl_kontakt_kontakt_trgm ON public.tbl_kontakt USING GIN (COALESCE(kontakt, '') gin_trgm_ops);";
if (!$db->db_query($qry))
echo '<strong>public.tbl_kontakt ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_kontakt: added index "idx_tbl_kontakt_kontakt_trgm"<br>';
}
}
// Add index for vorname to public.tbl_person
if ($result = @$db->db_query("SELECT 1
FROM pg_indexes WHERE indexname = 'idx_tbl_person_vorname_trgm';"))
{
if ($db->db_num_rows($result) == 0)
{
$qry = "CREATE INDEX idx_tbl_person_vorname_trgm ON public.tbl_person USING GIN (COALESCE(vorname, '') gin_trgm_ops);";
if (!$db->db_query($qry))
echo '<strong>public.tbl_person ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_person: added index "idx_tbl_person_vorname_trgm"<br>';
}
}
// Add index for nachname to public.tbl_person
if ($result = @$db->db_query("SELECT 1
FROM pg_indexes WHERE indexname = 'idx_tbl_person_nachname_trgm';"))
{
if ($db->db_num_rows($result) == 0)
{
$qry = "CREATE INDEX idx_tbl_person_nachname_trgm ON public.tbl_person USING GIN (COALESCE(nachname, '') gin_trgm_ops);";
if (!$db->db_query($qry))
echo '<strong>public.tbl_person ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_person: added index "idx_tbl_person_nachname_trgm"<br>';
}
}
// Add index for vorname || ' ' || nachname to public.tbl_person
if ($result = @$db->db_query("SELECT 1
FROM pg_indexes WHERE indexname = 'idx_tbl_person_name_trgm';"))
{
if ($db->db_num_rows($result) == 0)
{
$qry = "CREATE INDEX idx_tbl_person_name_trgm ON public.tbl_person USING GIN (COALESCE((vorname || ' ' || nachname), '') gin_trgm_ops);";
if (!$db->db_query($qry))
echo '<strong>public.tbl_person ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_person: added index "idx_tbl_person_name_trgm"<br>';
}
}
// Add Vector Indexes
// Add index for fts_bezeichnung to public.tbl_organisationseinheit
if (!$db->db_num_rows(@$db->db_query("SELECT 1
FROM pg_indexes WHERE indexname = 'idx_tbl_organisationseinheit_fts_bezeichnung_vector' LIMIT 1;")))
{
$qry = "CREATE INDEX idx_tbl_organisationseinheit_fts_bezeichnung_vector ON public.tbl_organisationseinheit USING GIN (fts_bezeichnung);";
if (!$db->db_query($qry))
echo '<strong>public.tbl_organisationseinheit ' . $db->db_last_error() . '</strong><br>';
else
echo 'public.tbl_organisationseinheit: added index "idx_tbl_organisationseinheit_fts_bezeichnung_vector"<br>';
}