Conflicts resolved

This commit is contained in:
Ivymaster
2026-08-04 09:08:12 +02:00
29 changed files with 1727 additions and 539 deletions
+2
View File
@@ -135,6 +135,8 @@ $route['lvplan/html/detail'] = 'LvPlanHTMLDetail';
// load routes from extensions, also look for environment-specific configs
$subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions'];
$route['api/frontend/v1/tempus/Tags/getTagsByCalendar/(:num)'] = 'api/frontend/v1/tempus/Tags/getTagsByCalendar/$1';
foreach($subdirs as $subdir)
{
if(is_dir($subdir))
+8 -1
View File
@@ -2,4 +2,11 @@
$config['send_update_mails'] = false;
$config['calendar_start'] = 7;
$config['calendar_end'] = 23;
$config['calendar_end'] = 23;
$config['tempus_tags'] = [
'prioone' => ['readonly' => false],
'priotwo' => ['readonly' => true],
'hinweis' => ['readonly' => false],
];
@@ -0,0 +1,53 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class Tags extends Tag_Controller
{
const BERECHTIGUNG_KURZBZ = ['admin:rw', 'assistenz:rw'];
public function __construct()
{
parent::__construct([
'getTag' => self::BERECHTIGUNG_KURZBZ,
'getTags' => self::BERECHTIGUNG_KURZBZ,
'getTagsByCalendar' => self::BERECHTIGUNG_KURZBZ,
'addTag' => self::BERECHTIGUNG_KURZBZ,
'updateTag' => self::BERECHTIGUNG_KURZBZ,
'doneTag' => self::BERECHTIGUNG_KURZBZ,
'deleteTag' => self::BERECHTIGUNG_KURZBZ
]);
$this->config->load('tempus');
}
public function getTag($readonly_tags = null)
{
parent::getTag($this->config->item('tempus_tags'));
}
public function getTags($tags = null)
{
parent::getTags($this->config->item('tempus_tags'));
}
public function getTagsByCalendar($eindeutige_kalender_gruppen_id = null)
{
parent::getTagsByAssignmentTypeValue('eindeutige_kalender_gruppen_id', $eindeutige_kalender_gruppen_id, $this->config->item('tempus_tags'));
}
public function addTag($withZuordnung = true, $updatable_tags = null)
{
parent::addTag(true, $this->config->item('tempus_tags'));
}
public function updateTag($updatable_tags = null)
{
parent::updateTag($this->config->item('tempus_tags'));
}
public function deleteTag($withZuordnung = true, $updatable_tags = null)
{
parent::deleteTag(true, $this->config->item('tempus_tags'));
}
public function doneTag($updatable_tags = null)
{
parent::doneTag($this->config->item('tempus_tags'));
}
}
@@ -32,6 +32,36 @@ class MigrateKalender extends CLI_Controller
/**
* Everything has a beginning
*/
public function refreshTablesAndFullMigrateOfStundenplanReservierung($von, $bis = null, $studiengang_kz = null, $ort_kurzbz = null)
{
echo "Refreshing calendar related tables...\n";
$this->resetBeforeNewStundenplanReservierungImport();
echo "Calendar related tables refreshed successfully.\n\n";
echo "Starting full migration of Stundenplan and Reservierung from $von to " . ($bis ?? '2100-12-31') . "...\n";
$this->fullMigrateOfStundenplanReservierung($von, $bis, $studiengang_kz, $ort_kurzbz);
echo "Full migration of Stundenplan and Reservierung completed successfully.\n";
}
public function fullMigrateOfStundenplanReservierung($von, $bis = null, $studiengang_kz = null, $ort_kurzbz = null)
{
echo "Starting migration of Stundenplan from $von to " . ($bis ?? '2100-12-31') . "...\n";
$this->migrateStundenplan($von, $bis, $studiengang_kz);
echo "Stundenplan migration completed successfully.\n\n";
echo "Starting migration of Reservierung from $von to " . ($bis ?? '2100-12-31') . "...\n";
$this->migrateReservierung($von, $bis, $ort_kurzbz);
echo "Reservierung migration completed successfully.\n\n";
echo "Starting migration of Stundenplan Betriebsmittel entries...\n";
$this->migrateStundenplanBetriebsmittelEntries();
echo "Stundenplan Betriebsmittel entries migration completed successfully.\n\n";
echo "Starting to add title values as tags to calendar entries...\n";
$this->addTitleValuesAsTagsToKalenderEntries();
echo "Adding title values as tags to calendar entries completed successfully.\n\n";
}
public function migrateStundenplan($von, $bis = null, $studiengang_kz = null)
{
if (is_null($von))
@@ -268,8 +298,9 @@ class MigrateKalender extends CLI_Controller
* Removes calendar entries created by the Stundenplan and Reservierung migrations,
* so that both imports can be started from scratch.
*/
public function resetBeforeNewStundenplandevImport()
public function resetBeforeNewStundenplanReservierungImport()
{
$db = new DB_Model();
$db->db->trans_start();
@@ -305,6 +336,28 @@ class MigrateKalender extends CLI_Controller
WHERE typ IN ('lehreinheit', 'reservierung')
)");
$db->db->query("DELETE FROM public.tbl_notiz
WHERE notiz_id IN (
SELECT notiz_id
FROM public.tbl_notizzuordnung
WHERE eindeutige_kalender_gruppen_id IN (
SELECT eindeutige_gruppen_id
FROM lehre.tbl_kalender
)
)");
$db->db->query("DELETE FROM public.tbl_notizzuordnung
WHERE eindeutige_kalender_gruppen_id IN (
SELECT eindeutige_gruppen_id
FROM lehre.tbl_kalender
)");
$db->db->query("DELETE FROM public.tbl_betriebsmittel_kalender
WHERE eindeutige_kalender_gruppen_id IN (
SELECT eindeutige_gruppen_id
FROM lehre.tbl_kalender
)");
$db->db->query("DELETE FROM lehre.tbl_kalender
WHERE typ IN ('lehreinheit', 'reservierung')");
@@ -371,6 +424,45 @@ class MigrateKalender extends CLI_Controller
$dbModel->db->query($query);
}
public function addTitleValuesAsTagsToKalenderEntries() {
$dbModel = new DB_Model();
$query = "SELECT
k.eindeutige_gruppen_id,
ARRAY_AGG(DISTINCT(spd.titel) ORDER BY spd.titel) AS titel,
MAX(spd.updateamum) AS updateamum
FROM lehre.tbl_stundenplandev spd
JOIN sync.tbl_stundenplandev_kalender spdk ON spd.stundenplandev_id = spdk.stundenplandev_id
JOIN lehre.tbl_kalender k ON k.kalender_id = spdk.kalender_id
WHERE k.typ = 'lehreinheit' AND k.eindeutige_gruppen_id IS NOT NULL AND spd.titel IS NOT NULL
GROUP BY k.eindeutige_gruppen_id";
$result = $dbModel->execReadOnlyQuery($query);
if (isError($result)) {
error("Error while fetching title values for adding as tags: " . getError($result));
return;
}
if (!hasData($result)) {
error("No title values found for adding as tags.");
return;
}
$data = getData($result);
foreach($data as $block) {
$eindeutige_gruppen_id = $block->eindeutige_gruppen_id;
$notizText = implode(' - ', array_filter($block->titel, function($titel) {
return !empty(trim($titel));
}));
if (!empty($notizText)) {
$this->addTag($eindeutige_gruppen_id, $notizText, $block->updateamum);
}
}
}
private function setKalendarEntriesGroupIDs() {
$dbModel = new DB_Model();
@@ -645,4 +737,129 @@ class MigrateKalender extends CLI_Controller
);
}
}
private function addTag($eindeutige_kalender_gruppen_id, $notizText, $lastUpdatedInOldSystem): bool
{
$notizType = 'hinweis';
$insertvonMockUser = 'oldToNewTempusMigration';
if (is_null($eindeutige_kalender_gruppen_id) || empty($eindeutige_kalender_gruppen_id)) {
var_dump("Error: eindeutige_kalender_gruppen_id is null or empty.");
return false;
}
$this->load->model('person/Notiz_model', 'NotizModel');
$this->load->model('system/Notiztyp_model', 'NotiztypModel');
$this->load->model('person/Notizzuordnung_model', 'NotizzuordnungModel');
$checkTyp = $this->NotiztypModel->loadWhere(array('typ_kurzbz' => $notizType));
if (isError($checkTyp))
{
error("Error occurred while checking Notiztyp: " . $checkTyp->message);
return false;
}
if (!hasData($checkTyp))
{
error("Notiztyp 'hinweis' does not exist. Please create it before adding tags.");
return false;
}
$this->NotizzuordnungModel->addJoin('tbl_notiz', 'tbl_notiz.notiz_id = tbl_notizzuordnung.notiz_id', 'LEFT');
$this->NotizzuordnungModel->db->where('tbl_notiz.typ', $notizType);
$this->NotizzuordnungModel->db->where('tbl_notiz.insertvon', $insertvonMockUser);
$oldNotizZuordnung = $this->NotizzuordnungModel->loadWhere(array('eindeutige_kalender_gruppen_id' => $eindeutige_kalender_gruppen_id));
if (isError($oldNotizZuordnung))
{
error("Error occurred while checking Notizzuordnung: " . $oldNotizZuordnung->message);
return false;
}
if (hasData($oldNotizZuordnung))
{
return $this->updateTag($eindeutige_kalender_gruppen_id, $notizText, $lastUpdatedInOldSystem);
}
$insertResult = $this->NotizModel->insert(array(
'titel' => 'TAG',
'text' => $notizText,
'verfasser_uid' => null,
'erledigt' => false,
'insertamum' => date('Y-m-d H:i:s'),
'insertvon' => $insertvonMockUser,
'typ' => $notizType
));
if (isError($insertResult))
{
error("Error occurred while inserting Notiz: " . $insertResult->message);
return false;
}
$insertZuordnung = $this->NotizzuordnungModel->insert(array(
'notiz_id' => $insertResult->retval,
'eindeutige_kalender_gruppen_id' => $eindeutige_kalender_gruppen_id
));
if (isError($insertZuordnung))
{
error("Error occurred while inserting Notizzuordnung: " . $insertZuordnung->message);
return false;
}
return true;
}
private function updateTag($eindeutige_kalender_gruppen_id, $notizText, $lastUpdatedInOldSystem)
{
$notizType = 'hinweis';
$insertvonMockUser = 'oldToNewTempusMigration';
$this->NotizzuordnungModel->addJoin('tbl_notiz', 'tbl_notiz.notiz_id = tbl_notizzuordnung.notiz_id', 'LEFT');
$this->NotizzuordnungModel->db->where('tbl_notiz.typ', $notizType);
$this->NotizzuordnungModel->db->where('tbl_notiz.insertvon', $insertvonMockUser);
$notizZuordnungRes = $this->NotizzuordnungModel->loadWhere(array('eindeutige_kalender_gruppen_id' => $eindeutige_kalender_gruppen_id));
if (isError($notizZuordnungRes))
{
error("Error occurred while loading Notizzuordnung: " . $notizZuordnungRes->message);
return false;
}
$tag = $this->NotizModel->loadWhere(array('notiz_id' => $notizZuordnungRes->retval[0]->notiz_id));
if (isError($tag))
{
error("Error occurred while loading Notiz: " . $tag->message);
return false;
}
if (!hasData($tag))
{
error("Error occurred while loading Notiz: " . $tag->message);
return false;
}
$adjustedLastUpdatedInOldSystem = date('Y-m-d H:i:s', strtotime($lastUpdatedInOldSystem) + 5);
if ($tag->retval[0]->updateamum >= $adjustedLastUpdatedInOldSystem)
{
return true;
}
$updateData = $this->NotizModel->update(array('notiz_id' => $tag->retval[0]->notiz_id),
array('text' => $notizText,
'updateamum' => date('Y-m-d H:i:s'),
'updatevon' => $insertvonMockUser,
'bearbeiter_uid' => null,
)
);
if (isError($updateData))
{
error("Error occurred while updating Notiz: " . $updateData->message);
return false;
}
return true;
}
}
+38 -4
View File
@@ -38,6 +38,8 @@ class Tag_Controller extends FHCAPI_Controller
public function getTag($readonly_tags = null)
{
$language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$id = $this->input->get('id');
if (is_array($readonly_tags) && !isEmptyArray($readonly_tags))
@@ -62,7 +64,7 @@ class Tag_Controller extends FHCAPI_Controller
$this->NotizModel->addSelect(
"tbl_notiz.titel,
tbl_notiz.text,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $language. " as bezeichnung,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $index_bezeichnung_mehrsprachig. " as bezeichnung,
tbl_notiz.notiz_id,
tbl_notiz_typ.style,
tbl_notiz.erledigt as done,
@@ -87,9 +89,12 @@ class Tag_Controller extends FHCAPI_Controller
public function getTags($tags = null)
{
$language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$this->NotiztypModel->addSelect(
'typ_kurzbz as tag_typ_kurzbz,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $index_bezeichnung_mehrsprachig. ' as bezeichnung,
style,
beschreibung,
tag
@@ -106,7 +111,36 @@ class Tag_Controller extends FHCAPI_Controller
$notiztypen = $this->NotiztypModel->loadWhere(array('aktiv' => true));
$this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array());
}
public function getTagsByAssignmentTypeValue($zuordnung_typ, $zuordnung_id, $tags = null)
{
$language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$this->NotizzuordnungModel->addSelect(
'tbl_notizzuordnung.notiz_id as notiz_id,
typ_kurzbz as tag_typ_kurzbz,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $index_bezeichnung_mehrsprachig. ' as bezeichnung,
style,
beschreibung,
tag,
tbl_notiz.erledigt as done
'
);
if (is_array($tags) && !isEmptyArray($tags))
{
$tags = $this->_filterTag($tags, false);
$this->NotizzuordnungModel->db->where_in('tbl_notiz_typ.typ_kurzbz', $tags);
}
$this->NotizzuordnungModel->addJoin('public.tbl_notiz', 'public.tbl_notizzuordnung.notiz_id = public.tbl_notiz.notiz_id');
$this->NotizzuordnungModel->addJoin('public.tbl_notiz_typ', 'public.tbl_notiz.typ = public.tbl_notiz_typ.typ_kurzbz');
$this->NotizzuordnungModel->addOrder('prioritaet');
$notiztypen = $this->NotizzuordnungModel->loadWhere(array('aktiv' => true, $zuordnung_typ => $zuordnung_id));
$this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array());
}
public function addTag($withZuordnung = true, $updatable_tags = null)
{
$postData = $this->getPostJson();
@@ -126,7 +160,7 @@ class Tag_Controller extends FHCAPI_Controller
if (!in_array($postData->tag_typ_kurzbz, $tags))
$this->terminateWithError($this->p->t('ui', 'keineBerechtigung'));
}
if ($withZuordnung)
{
$return = array();
@@ -135,7 +169,7 @@ class Tag_Controller extends FHCAPI_Controller
$this->terminateWithError('Error occurred', self::ERROR_TYPE_GENERAL);
$values = array_unique($postData->values);
foreach ($values as $value)
{
$insertResult = $this->addNotiz($postData);
+67 -19
View File
@@ -169,6 +169,69 @@ class KalenderLib
);
$this->_ci->KalenderModel->addJoin('public.tbl_studiengang le_studiengang', 'le_lehrverband.studiengang_kz = le_studiengang.studiengang_kz', 'LEFT');
// Include tags in the query
$this->_ci->load->config('tempus');
$tags = $this->_ci->config->item('tempus_tags');
$whereTags = '';
if (is_array($tags) && !isEmptyArray($tags)) {
$tags = array_keys($tags);
foreach ($tags as $key => $tag) {
$tags[$key] = $this->_ci->KalenderModel->escape($tag);
}
$whereTags = " AND nt.typ_kurzbz IN (" . implode(",", $tags) . ")";
}
$subQueryTag = "(
SELECT
tag.eindeutige_kalender_gruppen_id,
COALESCE(json_agg(tag ORDER BY tag.done, tag.prioritaet), '[]'::json) AS tags
FROM (
SELECT DISTINCT ON (n.notiz_id)
n.notiz_id AS id,
nt.typ_kurzbz,
array_to_json(nt.bezeichnung_mehrsprachig)->>0 AS beschreibung,
n.text AS notiz,
nt.style,
n.erledigt AS done,
nt.prioritaet AS prioritaet,
nz.eindeutige_kalender_gruppen_id
FROM public.tbl_notizzuordnung AS nz
JOIN public.tbl_notiz AS n ON nz.notiz_id = n.notiz_id
JOIN public.tbl_notiz_typ AS nt ON n.typ = nt.typ_kurzbz " . $whereTags . "
) AS tag
GROUP BY tag.eindeutige_kalender_gruppen_id
) AS tag_data_agg";
$this->_ci->KalenderModel->addJoin($subQueryTag, 'tag_data_agg.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id', 'LEFT');
// End of tags inclusion
// Include assigned operational resources in the query
$subQueryResource = "(
SELECT
resource.eindeutige_kalender_gruppen_id,
COALESCE(json_agg(resource ORDER BY resource.beschreibung), '[]'::json) AS resources
FROM (
SELECT
betriebsmittel_kalender.*,
betriebsmittel.beschreibung,
betriebsmittel.verplanen
FROM lehre.tbl_betriebsmittel_kalender AS betriebsmittel_kalender
JOIN wawi.tbl_betriebsmittel AS betriebsmittel
ON betriebsmittel.betriebsmittel_id = betriebsmittel_kalender.betriebsmittel_id
WHERE betriebsmittel.verplanen = TRUE
) AS resource
GROUP BY resource.eindeutige_kalender_gruppen_id
) AS resource_data_agg";
$this->_ci->KalenderModel->addJoin(
$subQueryResource,
'resource_data_agg.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id',
'LEFT'
);
// End of operational resources inclusion
$this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date);
$this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date);
}
@@ -190,6 +253,8 @@ class KalenderLib
{
$von = new DateTime($row->von);
$bis = new DateTime($row->bis);
$resources = isset($row->resources) ? $row->resources : [];
$parsedResources = is_string($resources) ? json_decode($resources, true) : $resources;
$events[$id] = (object) [
'kalender_id' => $id,
@@ -235,6 +300,8 @@ class KalenderLib
'collisions' => false,
'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false,
'updateamum' => isset($row->updateamum) ? $row->updateamum : null,
'resources' => is_array($parsedResources) ? $parsedResources : [],
'tags' => isset($row->tags) ? $row->tags : [],
];
}
@@ -566,15 +633,6 @@ class KalenderLib
$this->_ci->KalenderModel->db->where_not_in('status_kurzbz', array('deleted'));
$this->_ci->KalenderModel->addSelect([
"(
SELECT COUNT(*) FROM lehre.tbl_betriebsmittel_kalender
WHERE tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id
) AS has_assigned_resources"
]);
$this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_gruppen_id', 'DESC');
$data = $this->_ci->KalenderModel->load();
return $this->_mapEvents($data);
@@ -1383,16 +1441,6 @@ class KalenderLib
}
}
/* foreach ($specialGroups as $group)
{
$this->_addSpecialGroupToEvent($kalender_id, $group['gruppe_kurzbz'], $group['rolle']);
}
foreach ($groups as $group)
{
$this->_addGroupToEvent($kalender_id, $group['stg_kz'], $group['semester'], $group['verband'], $group['gruppe'], $group['rolle']);
}*/
if (isSuccess($kalendereventresult) && !is_null($ort_kurzbz))
{
$ortresult = $this->_addKalenderOrt($kalender_id, $ort_kurzbz);
+1
View File
@@ -9,6 +9,7 @@
'tabulator5' => true,
'vuedatepicker11' => true,
'momentjs2' => true,
'tags' => true,
'phrases' => array(
'global',
'ui',
@@ -86,6 +86,9 @@
pointer-events: none;
background-color: transparent !important;
border: none !important;
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.fhc-resize-bar--top {
+7
View File
@@ -17,6 +17,13 @@
transform: scale(1.15);
}
/* add prop that disasbles hover and cursor*/
.tag.disabled:hover
{
transform: none;
cursor: default;
}
.tag_rot {
background-color: #ff0000ff;
}
+59
View File
@@ -0,0 +1,59 @@
export default {
getTag(data)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Tags/getTag',
params: data
};
},
getTags(data)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Tags/getTags'
};
},
getTagsByCalendar(calenderGroupId)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Tags/getTagsByCalendar/' + calenderGroupId,
};
},
addTag(data)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Tags/addTag',
params: data
};
},
updateTag(data)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Tags/updateTag',
params: data
};
},
doneTag(data)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Tags/doneTag',
params: data
};
},
deleteTag(data)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Tags/deleteTag',
params: data
};
},
};
+10 -6
View File
@@ -14,6 +14,10 @@ export default {
'deleted',
],
props: {
isListItemShown: {
type: Boolean,
default: true
},
endpoint: {
type: Object,
required: true
@@ -71,7 +75,7 @@ export default {
'id': tag_id
};
this.$api.call(this.endpoint.getTag(getData))
await this.$api.call(this.endpoint.getTag(getData))
.then(result => result.data)
.then(result => this.openModal(result))
},
@@ -119,7 +123,7 @@ export default {
{
postData.id = this.selectedTagId;
this.tagData.id = this.selectedTagId;
this.$api.call(this.endpoint.updateTag(postData));
await this.$api.call(this.endpoint.updateTag(postData));
this.$emit("updated", this.tagData);
this.$refs.tagModal.hide();
}
@@ -131,7 +135,7 @@ export default {
return;
}
this.$api.call(this.endpoint.addTag(postData))
await this.$api.call(this.endpoint.addTag(postData))
.then(response => response.data)
.then(response => {
if (typeof response === 'number') {
@@ -158,7 +162,7 @@ export default {
done: !this.tagData.done,
notiz: this.tagData.notiz,
}
this.$api.call(this.endpoint.doneTag(postData))
await this.$api.call(this.endpoint.doneTag(postData))
this.$emit("updated", this.tagData);
this.$refs.tagModal.hide();
},
@@ -167,7 +171,7 @@ export default {
let postData = {
id: this.selectedTagId
}
this.$api.call(this.endpoint.deleteTag(postData))
await this.$api.call(this.endpoint.deleteTag(postData))
this.$emit("deleted", this.selectedTagId)
this.$refs.tagModal.hide();
},
@@ -206,7 +210,7 @@ export default {
}
},
template: `
<div class="plus_button_container" @mouseleave="hideList">
<div v-if="isListItemShown" class="plus_button_container" @mouseleave="hideList">
<span :title="values.length === 0 ? 'Bitte Zeilen markieren' : ''">
<button @mouseover="showList = true"
:disabled="!values || values.length === 0"
+24
View File
@@ -0,0 +1,24 @@
import AppMenu from "../AppMenu.js";
export default {
name: "TempusAppMenu",
components: {
AppMenu,
},
template: /* html */ `
<aside id="appMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header">
Tempus
<button
type="button"
class="btn-close text-reset"
data-bs-dismiss="offcanvas"
:aria-label="$p.t('ui/schliessen')"
></button>
</div>
<div class="offcanvas-body">
<app-menu app-identifier="tempus" />
</div>
</aside>
`,
};
+109
View File
@@ -0,0 +1,109 @@
import CoreSearchbar from "../searchbar/searchbar.js";
import NavLanguage from "../navigation/Language.js";
export default {
name: "TempusHeader",
components: {
CoreSearchbar,
NavLanguage,
},
props: {
tempusRoot: String,
avatarUrl: String,
logoutUrl: String,
searchbaroptions: {
type: Object,
required: true,
},
searchfunction: {
type: Function,
required: true,
},
},
emits: ["focus-searchbar"],
methods: {
focusSearchbar() {
this.$refs.searchbar?.$refs?.input?.focus();
this.$emit("focus-searchbar");
},
},
template: /* html */ `
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
class="btn btn-outline-light border-0 m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#appMenu"
aria-controls="appMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0" :href="tempusRoot">Tempus</a>
</div>
<button
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="fa-solid fa-table-list"></span>
</button>
<core-searchbar
ref="searchbar"
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
class="searchbar position-relative w-100"
show-btn-submit
></core-searchbar>
<div id="nav-user" class="dropdown">
<button
id="nav-user-btn"
class="btn btn-link rounded-0 py-0"
type="button"
data-bs-toggle="dropdown"
data-bs-target="#nav-user-menu"
aria-expanded="false"
aria-controls="nav-user-menu"
>
<img
:src="avatarUrl"
:alt="$p.t('profilUpdate/profilBild')"
class="bg-light avatar rounded-circle border border-light"
/>
</button>
<ul
ref="navUserDropdown"
class="dropdown-menu dropdown-menu-dark dropdown-menu-end rounded-0 text-center m-0"
aria-labelledby="nav-user-btn"
>
<li>
<button
type="button"
class="dropdown-item"
data-bs-toggle="modal"
data-bs-target="#configModal"
>
{{ $p.t('ui/settings') }}
</button>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<nav-language item-class="dropdown-item border-left-dark" />
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<a class="dropdown-item" :href="logoutUrl">
{{ $p.t('ui/logout') }}
</a>
</li>
</ul>
</div>
</header>
`,
};
@@ -0,0 +1,68 @@
/**
* 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 <https://www.gnu.org/licenses/>.
*/
import BsModal from "../Bootstrap/Modal.js";
export default {
name: "HistoryModal",
components: {
BsModal,
},
props: {
entries: {
type: Array,
default: () => [],
},
},
methods: {
show() {
this.$refs.modal.show();
},
hide() {
this.$refs.modal.hide();
},
},
template: `
<bs-modal
ref="modal"
class="bootstrap-prompt"
dialog-class="modal-lg"
data-cy="historyModal"
>
<template #title>History</template>
<template #default>
<table v-if="entries.length" class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Von</th>
<th>Bis</th>
<th>Status</th>
<th>Ort</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in entries" :key="entry.id">
<td>{{ entry.von }}</td>
<td>{{ entry.bis }}</td>
<td>{{ entry.status_kurzbz }}</td>
<td>{{ entry.ort }}</td>
</tr>
</tbody>
</table>
</template>
</bs-modal>
`,
};
@@ -77,7 +77,43 @@ export default {
return luxon.Duration
.fromISOTime(this.event.ende)
.toISOTime({ suppressSeconds: true });
}
},
tags() {
if (typeof this.event.tags === 'string') {
try {
return JSON.parse(this.event.tags);
} catch (e) {
console.error('Failed to parse tags:', e);
return [];
}
}
return this.event.tags || [];
},
tagsTooltip() {
if (this.tags.length === 0) {
return '';
}
return this.tags.map(tag => tag.beschreibung).join("\n");
},
resourcesTooltip() {
if (!this.event.resources || this.event.resources.length === 0) {
return '';
}
let resources = this.event.resources;
if (typeof this.event.resources === 'string') {
try {
resources = JSON.parse(this.event.resources);
} catch (e) {
console.error('Failed to parse resources:', e);
return '';
}
}
return resources.map(resource => resource.beschreibung).join("\n");
},
},
template: /*html*/`
<div
@@ -85,9 +121,16 @@ export default {
class="position-relative"
@wheel.stop
>
<div v-if="event.has_assigned_resources" class="position-absolute top-0 start-0 m-1" >
<div v-if="event.has_assigned_resources || tags?.length" class="position-absolute top-0 start-0 m-1 d-flex gap-1" >
<i
class="fa-solid fa-table-list text-muted"
v-tooltip="resourcesTooltip"
v-if="event.has_assigned_resources"
class="fa-solid fa-table-list text-muted"
></i>
<i
v-tooltip="tagsTooltip"
v-if="tags?.length"
class="fa-solid fa-tags"
></i>
</div>
<div
@@ -53,6 +53,31 @@ export default {
ortString() {
return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz;
},
tags() {
if (typeof this.event.tags === 'string') {
try {
return JSON.parse(this.event.tags);
} catch (e) {
console.error('Failed to parse tags:', e);
return [];
}
}
return this.event.tags || [];
},
resourcesValue() {
let resources = this.event.resources;
if (typeof this.event.resources === 'string') {
try {
resources = JSON.parse(this.event.resources);
} catch (e) {
console.error('Failed to parse resources:', e);
return null;
}
}
return resources.map(resource => resource.beschreibung).join(', ');
},
},
methods: {
mehtodNumberPadding: function (number) {
@@ -78,7 +103,7 @@ export default {
});
}
},
template: `
template: /*html*/`
<div>
<h5>
{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}
@@ -143,6 +168,33 @@ export default {
}}</th>
<td>{{event.organisationseinheit}}</td>
</tr>
<tr v-if="resourcesValue">
<th>{{
$p.t('ui','betriebsmittel')?
$p.t('ui','betriebsmittel')+':'
:''
}}</th>
<td>
{{ resourcesValue }}
</td>
</tr>
<tr v-if="tags.length">
<th>{{
$p.t('ui','tags')?
$p.t('ui','tags')+':'
:''
}}</th>
<td>
<div>
<span
v-for="tag in tags"
:key="tag.tag_typ_kurzbz"
:class="[tag.style, { tag_done: tag.done }]"
class="tag disabled"
>{{ tag.beschreibung }}</span>
</div>
</td>
</tr>
</tbody>
</table>
@@ -0,0 +1,282 @@
/**
* 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 <https://www.gnu.org/licenses/>.
*/
import BsModal from "../Bootstrap/Modal.js";
import FormInput from "../Form/Input.js";
import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js";
export default {
name: "ResourcesAssignmentModal",
components: {
BsModal,
FormInput,
},
emits: ["saveFinished"],
data() {
return {
calendar: null,
availableResources: [],
filteredAvailableResources: [],
selectedAvailableResource: null,
assignedResources: [],
areFormButtonsDisplayed: false,
};
},
computed: {
dropdownParsedAvailableResources() {
return this.availableResources
.map((unit) => ({
label: unit.beschreibung,
value: unit.betriebsmittel_id,
data: unit,
}))
.sort((a, b) => a.label?.localeCompare(b.label));
},
},
watch: {
selectedAvailableResource(newValue) {
if (!newValue) return;
this.assignedResources.push({
betriebsmittel_kalender_id: null,
betriebsmittel_id: newValue.data.betriebsmittel_id,
beschreibung: newValue.data.beschreibung,
anmerkung: "",
isNoteTextareaShown: false,
});
this.areFormButtonsDisplayed = true;
},
},
methods: {
async open(calendar) {
if (!calendar?.kalender_id) return;
this.calendar = calendar;
this.availableResources =
await this.fetchSchedulableResourcesByCalender(calendar.kalender_id);
this.filteredAvailableResources = [
...this.dropdownParsedAvailableResources,
];
this.assignedResources =
await this.fetchAssignedResourcesByCalender(calendar.kalender_id);
this.show();
},
show() {
this.$refs.modal.show();
},
hide() {
this.$refs.modal.hide();
},
reset() {
this.calendar = null;
this.availableResources = [];
this.filteredAvailableResources = [];
this.selectedAvailableResource = null;
this.assignedResources = [];
this.areFormButtonsDisplayed = false;
},
async fetchAssignedResourcesByCalender(calenderId) {
const result = await this.$api.call(
ApiOperationalResourceToCalender.getAssignedResourcesByCalender(
calenderId,
),
);
if (result.meta.status === "success") {
return result.data
.filter((unit) => !!unit)
.map((unit) => ({
isNoteTextareaShown:
unit.anmerkung && unit.anmerkung.trim() !== "",
...unit,
}));
}
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_resources_fetch_error_message"),
);
return [];
},
async fetchSchedulableResourcesByCalender(calendarId) {
const result = await this.$api.call(
ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar(
calendarId,
),
);
if (result.meta.status === "success") return result.data;
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"),
);
return [];
},
filterAvailableResources(event) {
const query = event.query.toLowerCase();
const unassignedResources = this.dropdownParsedAvailableResources.filter(
(unit) =>
!this.assignedResources.some(
(assigned) => assigned.betriebsmittel_id === unit.value,
),
);
this.filteredAvailableResources = query
? unassignedResources.filter((unit) =>
unit.label.toLowerCase().includes(query),
)
: unassignedResources;
},
toggleAssignedResourceNoteInput(resource) {
const assignedResource = this.assignedResources.find(
(assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id,
);
if (assignedResource) {
assignedResource.isNoteTextareaShown =
!assignedResource.isNoteTextareaShown;
}
this.areFormButtonsDisplayed = true;
},
removeAssignedResource(resource) {
this.assignedResources = this.assignedResources.filter(
(assigned) =>
assigned.betriebsmittel_id !== resource.betriebsmittel_id,
);
this.areFormButtonsDisplayed = true;
},
async refreshResourcesAssignmentModalData() {
if (!this.calendar?.kalender_id) return;
this.availableResources =
await this.fetchSchedulableResourcesByCalender(
this.calendar.kalender_id,
);
this.filteredAvailableResources = [
...this.dropdownParsedAvailableResources,
];
this.assignedResources = await this.fetchAssignedResourcesByCalender(
this.calendar.kalender_id,
);
this.selectedAvailableResource = null;
this.areFormButtonsDisplayed = false;
},
async saveAssignedResourcesToCalendarItem() {
if (!this.calendar?.kalender_id) return;
const result = await this.$api.call(
ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship(
this.calendar.kalender_id,
this.assignedResources,
),
);
if (result.meta.status === "success") {
this.$fhcAlert.alertSuccess(
this.$p.t("ui", "assigned_resources_save_success_message"),
);
await this.refreshResourcesAssignmentModalData();
} else {
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_resources_save_error_message"),
);
}
this.$emit("saveFinished");
this.hide();
},
},
template: `
<bs-modal
ref="modal"
@hide-bs-modal="reset"
class="bootstrap-prompt"
data-cy="resourcesAssignmentModal"
>
<template #title>{{ $p.t('ui', 'resource_assignment_modal_title') }}</template>
<template #default>
<div class="mb-5">
<form-input
v-if="availableResources.length"
@item-select="selectedAvailableResource = $event.value"
:label="$p.t('ui', 'available_resources_label')"
:suggestions="filteredAvailableResources"
:option-value="option => option.value"
:option-label="option => option.label"
@complete="filterAvailableResources"
dropdown
force-selection
type="autocomplete"
name="availableResources"
:close-on-select="false"
/>
</div>
<div>
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-2 mx-auto text-bold fw-1">
{{ $p.t('ui', 'assigned_resources_subtitle') }}
</h6>
</div>
<div v-if="assignedResources.length" class="mb-4">
<div
v-for="resource in assignedResources"
:key="resource.betriebsmittel_id"
class="shadow-sm p-2 mb-2 bg-body rounded"
>
<div class="d-flex justify-content-between align-items-center mb-1">
<p class="m-0">{{ resource.beschreibung }}</p>
<div class="d-flex justify-content-between align-items-center gap-2">
<a href="#" @click.prevent="toggleAssignedResourceNoteInput(resource)" class="ms-auto">
<i class="fa fa-edit text-primary"></i>
</a>
<a href="#" @click.prevent="removeAssignedResource(resource)" class="ms-auto">
<i class="fa fa-trash text-danger"></i>
</a>
</div>
</div>
<form-input
v-if="resource.isNoteTextareaShown"
v-model="resource.anmerkung"
@input="areFormButtonsDisplayed = true"
:placeholder="$capitalize($p.t('global/anmerkung'))"
:rows="1"
class="flex-grow-1"
type="textarea"
name="anmerkung"
/>
</div>
</div>
<div v-else class="d-flex align-items-center justify-content-center mb-2">
<p class="text-muted mb-0">{{ $p.t('ui', 'no_assigned_resources') }}</p>
</div>
<div
v-if="areFormButtonsDisplayed"
class="d-flex justify-content-end gap-2"
>
<button type="button" class="btn btn-secondary" @click="refreshResourcesAssignmentModalData">
{{ $p.t('ui', 'abbrechen') }}
</button>
<button type="button" class="btn btn-primary" @click="saveAssignedResourcesToCalendarItem">
{{ $p.t('ui', 'speichern') }}
</button>
</div>
</div>
</template>
</bs-modal>
`,
};
+168
View File
@@ -0,0 +1,168 @@
import FhcCoursepicker from "./Coursepicker.js";
import LectureSelection from "./Filters/LectureSelection.js";
import VerbandSelection from "./Filters/VerbandSelection.js";
import RoomSelection from "./Filters/RoomSelection.js";
import ParkingSlot from "./ParkingSlot.js";
import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js";
export default {
name: "TempusSidebarMenu",
components: {
FhcCoursepicker,
LectureSelection,
VerbandSelection,
RoomSelection,
ParkingSlot,
StvStudiensemester,
},
props: {
previewRole: {
type: String,
required: true,
},
rooms: {
type: Array,
required: true,
},
studiengaenge: {
type: Array,
required: true,
},
studiengaengeAll: {
type: Array,
default: () => [],
},
lecturers: {
type: Array,
required: true,
},
selectedStudiensemester: {
type: String,
required: true,
},
},
emits: [
"update:previewRole",
"update:rooms",
"update:studiengaenge",
"update:parkedKeys",
"update:selectedStudiensemester",
"remove-lecturer",
"select-lecturer",
"select-kw",
"sync",
],
methods: {
reloadCoursepicker() {
return this.$refs.coursepicker?.reload();
},
isParked(id) {
return this.$refs.parking?.isParked(id) ?? false;
},
park(items) {
return this.$refs.parking?.park(null, items);
},
unpark(item) {
return this.$refs.parking?.unpark(item);
},
},
template: /* html */ `
<nav
id="sidebarMenu"
class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100 d-flex flex-column"
>
<div class="sidebar-icons d-flex flex-row align-items-start py-2 gap-1 ps-2">
<button
class="btn btn-outline-secondary"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#verbandMenu"
aria-controls="verbandMenu"
aria-expanded="false"
title="Verband"
>
<span class="fa-solid fa-university"></span>
</button>
<button
class="btn btn-outline-secondary"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#verbandMenu"
aria-controls="verbandMenu"
aria-expanded="false"
title="Verband"
>
<span class="fa-solid fa-door-open"></span>
</button>
</div>
<div class="px-2 py-1 w-100">
<div
class="d-flex gap-1 py-1"
data-cy="previewRoleOptionsHolder"
>
<button
class="btn btn-sm"
:class="previewRole === 'planer' ? 'btn-dark' : 'btn-outline-dark'"
@click="$emit('update:previewRole', 'planer')"
>
<i class="fa-solid fa-pen-ruler me-1"></i>Planer
</button>
<button
class="btn btn-sm"
:class="previewRole === 'lektor' ? 'btn-primary' : 'btn-outline-primary'"
@click="$emit('update:previewRole', 'lektor')"
>
<i class="fa-solid fa-chalkboard-user me-1"></i>Lektor
</button>
<button
class="btn btn-sm"
:class="previewRole === 'student' ? 'btn-success' : 'btn-outline-success'"
@click="$emit('update:previewRole', 'student')"
>
<i class="fa-solid fa-user-graduate me-1"></i>Student
</button>
<button
class="btn btn-sm btn-outline-danger"
@click="$emit('sync')"
>
<i class="fa-solid fa-rotate me-1"></i>Sync
</button>
</div>
</div>
<room-selection
v-if="rooms.length"
:rooms="rooms"
@update:rooms="$emit('update:rooms', $event)"
></room-selection>
<verband-selection
v-if="studiengaenge.length"
:studiengaenge="studiengaenge"
:studiengaenge-all="studiengaengeAll"
@update:studiengaenge="$emit('update:studiengaenge', $event)"
></verband-selection>
<lecture-selection
v-if="lecturers.length"
:lecturers="lecturers"
@remove="$emit('remove-lecturer', $event)"
></lecture-selection>
<div class="d-flex flex-column flex-grow-1" style="min-height: 0">
<parking-slot
ref="parking"
@update:parked-keys="$emit('update:parkedKeys', $event)"
></parking-slot>
<fhc-coursepicker
ref="coursepicker"
:studiengaenge="studiengaenge"
:studiensemester="selectedStudiensemester"
@select-lecturer="$emit('select-lecturer', $event)"
@select-kw="$emit('select-kw', $event)"
></fhc-coursepicker>
</div>
<stv-studiensemester
:studiensemester-kurzbz="selectedStudiensemester"
@update:studiensemester-kurzbz="$emit('update:selectedStudiensemester', $event)"
></stv-studiensemester>
</nav>
`,
};
@@ -0,0 +1,239 @@
/**
* 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 <https://www.gnu.org/licenses/>.
*/
import BsModal from "../Bootstrap/Modal.js";
import FormInput from "../Form/Input.js";
import CoreTag from "../Tag/Tag.js";
import ApiTempusTag from "../../api/factory/tempus/tag.js";
export default {
name: "TagsAssignmentModal",
components: {
BsModal,
FormInput,
CoreTag,
},
emits: ["tagsChanged"],
data() {
return {
calendar: null,
availableTags: [],
filteredAvailableTags: [],
selectedAvailableTag: null,
assignedTags: [],
tagEndpoint: ApiTempusTag,
tagModalEscapeHandler: null,
};
},
computed: {
dropdownParsedAvailableTags() {
return this.availableTags.map((tag) => ({
label: tag.bezeichnung,
value: tag.tag_typ_kurzbz,
data: tag,
}));
},
},
methods: {
async open(calendar) {
if (!calendar?.kalender_id) return;
this.calendar = calendar;
this.availableTags = await this.fetchAvailableTags();
this.filteredAvailableTags = [...this.dropdownParsedAvailableTags];
this.assignedTags = await this.fetchAssignedTagsByCalender(
calendar.eindeutige_gruppen_id,
);
this.show();
},
show() {
this.$refs.modal.show();
},
hide() {
this.$refs.modal.hide();
},
reset() {
this.removeTagModalEscapeHandler();
this.calendar = null;
this.availableTags = [];
this.filteredAvailableTags = [];
this.selectedAvailableTag = null;
this.assignedTags = [];
},
prepareTagModal() {
const tagModal = this.$refs.tagComponent?.$refs.tagModal;
const tagModalElement = tagModal?.$el;
if (!tagModalElement) return;
tagModalElement.addEventListener(
"shown.bs.modal",
() => {
this.tagModalEscapeHandler = (event) => {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopImmediatePropagation();
tagModal.hide();
};
document.addEventListener("keydown", this.tagModalEscapeHandler, true);
},
{ once: true },
);
tagModalElement.addEventListener(
"hidden.bs.modal",
() => this.restoreAssignmentModalFocus(),
{ once: true },
);
},
removeTagModalEscapeHandler() {
if (!this.tagModalEscapeHandler) return;
document.removeEventListener(
"keydown",
this.tagModalEscapeHandler,
true,
);
this.tagModalEscapeHandler = null;
},
restoreAssignmentModalFocus() {
this.removeTagModalEscapeHandler();
this.$refs.modal?.$el?.focus();
},
async fetchAvailableTags() {
const result = await this.$api.call(ApiTempusTag.getTags());
if (result.meta.status === "success") return result.data;
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_available_tags_fetch_error_message"),
);
return [];
},
async fetchAssignedTagsByCalender(calendarGroupId) {
const result = await this.$api.call(
ApiTempusTag.getTagsByCalendar(calendarGroupId),
);
if (result.meta.status === "success") {
return result.data.filter((tag) => !!tag);
}
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_tags_fetch_error_message"),
);
return [];
},
filterAvailableTags(event) {
const query = event.query.toLowerCase();
const unassignedTags = this.dropdownParsedAvailableTags.filter(
(tag) =>
!this.assignedTags.some(
(assigned) => assigned.tag_id === tag.value,
),
);
this.filteredAvailableTags = query
? unassignedTags.filter((tag) =>
tag.label.toLowerCase().includes(query),
)
: unassignedTags;
},
selectTag(event) {
this.selectedAvailableTag = event.value;
this.prepareTagModal();
this.$refs.tagComponent?.openModal(event.value.data);
},
editTag(tag) {
this.prepareTagModal();
this.$refs.tagComponent?.editTag(tag.notiz_id);
},
async handleCalendarTagChange() {
this.removeTagModalEscapeHandler();
const calendarGroupId = this.calendar?.eindeutige_gruppen_id;
if (!calendarGroupId) return;
this.assignedTags =
await this.fetchAssignedTagsByCalender(calendarGroupId);
this.$emit("tagsChanged");
},
},
template: /* html */`
<bs-modal
ref="modal"
@hide-bs-modal="reset"
body-class="p-4"
class="bootstrap-prompt"
data-cy="tagsAssignmentModal"
>
<template #title>{{ $p.t('ui', 'tags_assignment_modal_title') }}</template>
<template #default>
<div class="mb-5">
<form-input
v-if="availableTags.length"
@item-select="selectTag"
:label="$capitalize($p.t('ui', 'tags'))"
:suggestions="filteredAvailableTags"
:option-value="option => option.value"
:option-label="option => option.label"
@complete="filterAvailableTags"
dropdown
force-selection
type="autocomplete"
name="availableTags"
:close-on-select="false"
>
<template #option="{ option }">
<span :class="['tag', option.data.style]">{{ option.label }}</span>
</template>
</form-input>
</div>
<div>
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-1 mx-auto text-bold fw-1">
{{ $p.t('ui', 'assigned_tags_subtitle') }}
</h6>
</div>
<div v-if="assignedTags.length">
<span
v-for="tag in assignedTags"
:key="tag.tag_typ_kurzbz"
:class="[tag.style, { tag_done: tag.done }]"
@click="editTag(tag)"
class="tag"
>{{ tag.bezeichnung }}</span>
</div>
<div v-else class="d-flex align-items-center justify-content-center mb-2">
<p class="text-muted mb-0">{{ $p.t('ui', 'no_assigned_tags') }}</p>
</div>
</div>
</template>
</bs-modal>
<core-tag
v-if="calendar?.eindeutige_gruppen_id"
ref="tagComponent"
:is-list-item-shown="false"
:endpoint="tagEndpoint"
:values="[calendar.eindeutige_gruppen_id]"
zuordnung_typ="eindeutige_kalender_gruppen_id"
@added="handleCalendarTagChange"
@deleted="handleCalendarTagChange"
@updated="handleCalendarTagChange"
/>
`,
};
+79 -504
View File
@@ -14,60 +14,51 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import CoreSearchbar from "../searchbar/searchbar.js";
import NavLanguage from "../navigation/Language.js";
import VerticalSplit from "../verticalsplit/verticalsplit.js";
import FhcCalendar from "../Calendar/Tempus.js";
import FhcCoursepicker from "./Coursepicker.js";
import LectureSelection from "./Filters/LectureSelection.js";
import VerbandSelection from "./Filters/VerbandSelection.js";
import RoomSelection from "./Filters/RoomSelection.js";
import ParkingSlot from "./ParkingSlot.js";
import ApiKalender from "../../api/factory/tempus/kalender.js";
import ApiSearchbar from "../../api/factory/searchbar.js";
import ApiRenderers from "../../api/factory/renderers.js";
import ApiTempusConfig from "../../api/factory/tempus/config.js";
import ApiBetriebsmittel from "../../api/factory/betriebsmittel.js";
import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js";
import AppMenu from "../AppMenu.js";
import drop from "../../directives/drop.js";
import AppConfig from "../AppConfig.js";
import BsModal from "../Bootstrap/Modal.js";
import StvVerband from "../Stv/Studentenverwaltung/Verband.js";
import ApiStudiengangTree from "../../api/factory/tempus/studiengangtree.js";
import ApiInfo from "../../api/factory/tempus/info.js";
import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js";
import FormInput from "../../../js/components/Form/Input.js";
import Reservierung from "./Reservierung.js";
import { getTempusShortcuts } from "./shortcuts.js";
import KeyboardShortcuts from "./KeyboardShortcuts.js";
import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js";
import MultiWeekPlanModal from "./MultiWeekPlanModal.js";
import HistoryModal from "./HistoryModal.js";
import ResourcesAssignmentModal from "./ResourcesAssignmentModal.js";
import TagsAssignmentModal from "./TagsAssignmentModal.js";
import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js";
import TempusHeader from "./Header.js";
import TempusAppMenu from "./AppMenu.js";
import TempusVerbandMenu from "./VerbandMenu.js";
import TempusSidebarMenu from "./SidebarMenu.js";
export default {
name: "Tempus",
components: {
CoreSearchbar,
VerticalSplit,
FhcCalendar,
FhcCoursepicker,
LectureSelection,
VerbandSelection,
RoomSelection,
ParkingSlot,
AppConfig,
AppMenu,
NavLanguage,
TempusAppMenu,
TempusHeader,
BsModal,
StvVerband,
StvStudiensemester,
TempusVerbandMenu,
TempusSidebarMenu,
Multiselect: primevue.multiselect,
FormInput,
Reservierung,
KeyboardShortcuts,
MultiWeekPlanModal,
HistoryModal,
ResourcesAssignmentModal,
TagsAssignmentModal,
},
props: {
defaultSemester: String,
@@ -92,8 +83,9 @@ export default {
appConfig: Vue.computed(() => this.appconfig),
contextMenuActions: useContextMenuActions({
openRaumauswahl: (orig) => this.openRaumauswahl(orig),
openResourcesAssignmentModal: (orig) =>
this.openResourcesAssignmentModal(orig),
openResourcesAssignmentModal: (orig) =>
this.$refs.resourcesAssignmentModal?.open(orig),
openTagsModal: (orig) => this.$refs.tagsAssignmentModal?.open(orig),
openHistory: (orig) => this.openHistory(orig),
deleteEntry: (orig) => this.deleteEntry(orig),
syncToLecturer: (orig) => this.syncToLecturer(orig),
@@ -161,14 +153,6 @@ export default {
endTime: null,
},
studiengaenge_all: [],
resourcesAssignmentModal: {
calendar: null,
availableResources: [],
filteredAvailableResources: [],
selectedAvailableResource: null,
assignedResources: [],
areFormButtonsDisplayed: false,
},
};
},
computed: {
@@ -203,17 +187,6 @@ export default {
searchbaroptions() {
return getTempusSearchbarOptions(this);
},
dropdownParsedAvailableResources() {
return this.resourcesAssignmentModal.availableResources
.map((unit) => {
return {
label: unit.beschreibung,
value: unit.betriebsmittel_id,
data: unit,
};
})
.sort((a, b) => a.label?.localeCompare(b.label));
},
},
methods: {
async openRaumauswahl(orig) {
@@ -227,27 +200,12 @@ export default {
this.$refs.raumModal.show();
});
},
async openResourcesAssignmentModal(orig) {
if (!orig?.kalender_id) return;
this.resourcesAssignmentModal.calendar = orig;
this.resourcesAssignmentModal.availableResources =
await this.fetchSchedulableResourcesByCalender(orig.kalender_id);
this.resourcesAssignmentModal.filteredAvailableResources = [
...this.dropdownParsedAvailableResources,
];
this.resourcesAssignmentModal.assignedResources =
await this.fetchAssignedResourcesByCalender(orig.kalender_id);
this.$refs.resourcesAssignmentModal.show();
},
async deleteEntry(orig) {
if (!orig?.kalender_id) return;
await this.deleteEntryCall(orig);
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.$refs.sidebar.reloadCoursepicker();
},
async deleteEntries(origList) {
let validList = (origList ?? []).filter((orig) => orig?.kalender_id);
@@ -257,13 +215,16 @@ export default {
validList.map((orig) => this.deleteEntryCall(orig)),
);
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.$refs.sidebar.reloadCoursepicker();
},
async deleteEntryCall(orig) {
await this.$api
.call(ApiKalender.deleteEntry(orig.kalender_id))
.then(() => {
this.$refs.parking.unpark({ type: orig.type, id: orig.kalender_id });
this.$refs.sidebar.unpark({
type: orig.type,
id: orig.kalender_id,
});
});
},
async openHistory(orig) {
@@ -272,7 +233,7 @@ export default {
.call(ApiKalender.getHistory(orig.kalender_id))
.then((result) => {
this.historyEntries = result.data ?? [];
this.$refs.historyModel.show();
this.$refs.historyModal.show();
});
},
syncToLecturer(orig) {
@@ -304,12 +265,6 @@ export default {
this.ort_kurzbz = data.ort_kurzbz;
this.rooms = [{ ort_kurzbz: data.ort_kurzbz }];
},
onSelectVerbandAndClose(payload) {
console.log(payload);
this.onSelectVerband(payload);
bootstrap.Offcanvas.getOrCreateInstance(this.$refs.verbandMenu).hide();
},
onSelectVerband({ link, studiengang_kz, semester, orgform_kurzbz, name }) {
if (orgform_kurzbz) {
semester = null;
@@ -570,7 +525,7 @@ export default {
dates.end_time,
() => {
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.$refs.sidebar.reloadCoursepicker();
},
);
},
@@ -639,7 +594,7 @@ export default {
.then((result) => result.data)
.then((result) => {
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.$refs.sidebar.reloadCoursepicker();
this.bcc.postMessage("dropped");
});
} else if (obj.type === "kalender") {
@@ -650,7 +605,7 @@ export default {
start_time,
end_time,
() => {
this.$refs.parking.unpark({
this.$refs.sidebar.unpark({
type: obj.type,
id: obj.orig.kalender_id,
});
@@ -674,7 +629,7 @@ export default {
},
onMultiWeekConfirmed() {
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.$refs.sidebar.reloadCoursepicker();
this.bcc.postMessage("dropped");
},
handleRange(range) {
@@ -787,138 +742,6 @@ export default {
this.extraBackgrounds = res;
},
async fetchAssignedResourcesByCalender(calenderId) {
let getAssignedResources = await this.$api.call(
ApiOperationalResourceToCalender.getAssignedResourcesByCalender(
calenderId,
),
);
if (getAssignedResources.meta.status === "success") {
return getAssignedResources.data
.filter((unit) => !!unit)
.map((unit) => {
return {
isNoteTextareaShown:
unit.anmerkung && unit.anmerkung.trim() !== "",
...unit,
};
})
.filter((unit) => !!unit);
} else {
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_resources_fetch_error_message"),
);
}
return [];
},
async fetchSchedulableResourcesByCalender(calendarID) {
let getSchedulableResourcesByCalendar = await this.$api.call(
ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar(
calendarID,
),
);
if (getSchedulableResourcesByCalendar.meta.status === "success") {
return getSchedulableResourcesByCalendar.data;
} else {
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"),
);
}
return [];
},
filterAvailableResources(event) {
this.resourcesAssignmentModal.filteredAvailableResources;
const query = event.query.toLowerCase();
if (!query) {
return (this.resourcesAssignmentModal.filteredAvailableResources = [
...this.dropdownParsedAvailableResources.filter((unit) => {
return !this.resourcesAssignmentModal.assignedResources.some(
(assigned) => assigned.betriebsmittel_id === unit.value,
);
}),
]);
}
return (this.resourcesAssignmentModal.filteredAvailableResources =
this.dropdownParsedAvailableResources
.filter((unit) => {
return !this.resourcesAssignmentModal.assignedResources.some(
(assigned) => assigned.betriebsmittel_id === unit.value,
);
})
.filter((unit) => {
return unit.label.toLowerCase().includes(query);
}));
},
toggleAssignedResourceNoteInput(resource) {
const index = this.resourcesAssignmentModal.assignedResources.findIndex(
(assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id,
);
if (index !== -1) {
this.resourcesAssignmentModal.assignedResources[
index
].isNoteTextareaShown =
!this.resourcesAssignmentModal.assignedResources[index]
.isNoteTextareaShown;
}
this.resourcesAssignmentModal.areFormButtonsDisplayed = true;
},
removeAssignedResource(resource) {
this.resourcesAssignmentModal.assignedResources =
this.resourcesAssignmentModal.assignedResources.filter(
(assigned) =>
assigned.betriebsmittel_id !== resource.betriebsmittel_id,
);
this.resourcesAssignmentModal.areFormButtonsDisplayed = true;
},
async refreshResourcesAssignmentModalData(calenderItem) {
this.resourcesAssignmentModal.availableResources =
await this.fetchSchedulableResourcesByCalender(
calenderItem.kalender_id,
);
this.resourcesAssignmentModal.filteredAvailableResources = [
...this.dropdownParsedAvailableResources,
];
this.resourcesAssignmentModal.assignedResources =
await this.fetchAssignedResourcesByCalender(calenderItem.kalender_id);
this.resourcesAssignmentModal.selectedAvailableResource = null;
this.resourcesAssignmentModal.areFormButtonsDisplayed = false;
},
async saveAssignedResourcesToCalendarItem(calenderItem, assignedResources) {
let getSchedulableResourcesByCalendar = await this.$api.call(
ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship(
calenderItem.kalender_id,
assignedResources,
),
);
if (getSchedulableResourcesByCalendar.meta.status === "success") {
this.$fhcAlert.alertSuccess(
this.$p.t("ui", "assigned_resources_save_success_message"),
);
await this.refreshResourcesAssignmentModalData(calenderItem);
} else {
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_resources_save_error_message"),
);
}
this.$refs.calendar.resetEventLoader();
this.$refs.resourcesAssignmentModal.hide();
},
closeResourcesAssignmentModal() {
this.resourcesAssignmentModal = {
availableResources: [],
filteredAvailableResources: [],
selectedAvailableResource: null,
assignedResources: [],
areFormButtonsDisplayed: false,
};
},
removeLecturer(uid) {
if (uid == null) {
for (const lect of this.lecturers) delete this.overlayCache[lect.uid];
@@ -933,6 +756,10 @@ export default {
this.$refs.calendar.resetEventLoader();
}
},
handlePreviewRoleChange(role) {
this.previewRole = role;
this.$refs.calendar.resetEventLoader();
},
triggerSync() {
this.$api
.call(ApiKalender.sync())
@@ -950,10 +777,13 @@ export default {
const event = this.hoveredEvent;
if (!event?.kalender_id) return;
if (this.$refs.parking.isParked(event.kalender_id)) {
this.$refs.parking.unpark({ type: event.type, id: event.kalender_id });
if (this.$refs.sidebar.isParked(event.kalender_id)) {
this.$refs.sidebar.unpark({
type: event.type,
id: event.kalender_id,
});
} else {
this.$refs.parking.park(null, {
this.$refs.sidebar.park({
type: "kalender",
id: event.kalender_id,
orig: event,
@@ -970,7 +800,7 @@ export default {
this.hoveredEvent = null;
},
focusSearchbar() {
this.$refs.searchbar?.$refs?.input.focus();
this.$refs.header?.focusSearchbar();
},
},
watch: {
@@ -988,19 +818,6 @@ export default {
handler() {
this.$refs.calendar.resetEventLoader();
},
},
"resourcesAssignmentModal.selectedAvailableResource": function (newVal) {
if (!newVal) return;
this.resourcesAssignmentModal.assignedResources.push({
betriebsmittel_kalender_id: null,
betriebsmittel_id: newVal.data.betriebsmittel_id,
beschreibung: newVal.data.beschreibung,
anmerkung: "",
isNoteTextareaShown: false,
});
this.resourcesAssignmentModal.areFormButtonsDisplayed = true;
},
},
mounted() {
@@ -1077,198 +894,41 @@ export default {
this.studiengaenge_all = res.data;
});
},
template: `
template: /* html */`
<div
class="tempus"
data-cy="tempus"
>
<keyboard-shortcuts :shortcuts="keyboardShortcuts" />
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
class="btn btn-outline-light border-0 m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#appMenu"
aria-controls="appMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0" :href="tempusRoot">Tempus</a>
</div>
<button
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="fa-solid fa-table-list"></span>
</button>
<core-searchbar
ref="searchbar"
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
class="searchbar position-relative w-100"
show-btn-submit
></core-searchbar>
<div id="nav-user" class="dropdown">
<button
id="nav-user-btn"
class="btn btn-link rounded-0 py-0"
type="button"
data-bs-toggle="dropdown"
data-bs-target="#nav-user-menu"
aria-expanded="false"
aria-controls="nav-user-menu"
>
<img
:src="avatarUrl"
:alt="$p.t('profilUpdate/profilBild')"
class="bg-light avatar rounded-circle border border-light"
/>
</button>
<ul
ref="navUserDropdown"
class="dropdown-menu dropdown-menu-dark dropdown-menu-end rounded-0 text-center m-0"
aria-labelledby="nav-user-btn"
>
<li>
<button
type="button"
class="dropdown-item"
data-bs-toggle="modal"
data-bs-target="#configModal"
>
{{ $p.t('ui/settings') }}
</button>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<nav-language
item-class="dropdown-item border-left-dark"
/>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<a class="dropdown-item" :href="logoutUrl">
{{ $p.t('ui/logout') }}
</a>
</li>
</ul>
</div>
</header>
<tempus-header
ref="header"
:tempus-root="tempusRoot"
:avatar-url="avatarUrl"
:logout-url="logoutUrl"
:searchbaroptions="searchbaroptions"
:searchfunction="searchfunction"
/>
<div class="container-fluid overflow-hidden heightfull">
<div class="row h-100">
<aside id="appMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header">
Tempus
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<div class="offcanvas-body">
<app-menu app-identifier="tempus" />
</div>
</aside>
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100 d-flex flex-column">
<div class="sidebar-icons d-flex flex-row align-items-start py-2 gap-1 ps-2">
<button
class="btn btn-outline-secondary"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#verbandMenu"
aria-controls="verbandMenu"
aria-expanded="false"
title="Verband"
>
<span class="fa-solid fa-university"></span>
</button>
<button
class="btn btn-outline-secondary"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#verbandMenu"
aria-controls="verbandMenu"
aria-expanded="false"
title="Verband"
>
<span class="fa-solid fa-door-open"></span>
</button>
</div>
<div class="px-2 py-1 w-100">
<!--<Multiselect
:model-value="visibleStatusValue"
@update:model-value="val => toggleStatus(val.map(o => o.key))"
option-label="label"
:options="visibleStatusOptions"
placeholder="Status filtern"
:hide-selected="false"
:show-toggle-all="false"
class="w-100"
/>-->
<div
class="d-flex gap-1 py-1"
data-cy="previewRoleOptionsHolder"
>
<button
class="btn btn-sm"
:class="previewRole === 'planer' ? 'btn-dark' : 'btn-outline-dark'"
@click="previewRole = 'planer'; $refs.calendar.resetEventLoader()"
>
<i class="fa-solid fa-pen-ruler me-1"></i>Planer
</button>
<button
class="btn btn-sm"
:class="previewRole === 'lektor' ? 'btn-primary' : 'btn-outline-primary'"
@click="previewRole = 'lektor'; $refs.calendar.resetEventLoader()"
>
<i class="fa-solid fa-chalkboard-user me-1"></i>Lektor
</button>
<button
class="btn btn-sm"
:class="previewRole === 'student' ? 'btn-success' : 'btn-outline-success'"
@click="previewRole = 'student'; $refs.calendar.resetEventLoader()"
>
<i class="fa-solid fa-user-graduate me-1"></i>Student
</button>
<button
class="btn btn-sm btn-outline-danger"
@click="triggerSync"
>
<i class="fa-solid fa-rotate me-1"></i>Sync
</button>
</div>
</div>
<room-selection
v-if="rooms.length"
v-model:rooms="rooms"
></room-selection>
<verband-selection
v-if="studiengaenge.length"
v-model:studiengaenge="studiengaenge"
:studiengaenge-all="studiengaenge_all"
></verband-selection>
<lecture-selection
v-if="lecturers.length"
:lecturers="lecturers"
@remove="removeLecturer"
></lecture-selection>
<div class="d-flex flex-column flex-grow-1" style="min-height: 0">
<parking-slot
ref="parking"
v-model:parked-keys="parkedKeys"
></parking-slot>
<fhc-coursepicker ref="coursepicker" :studiengaenge="studiengaenge" @select-lecturer="setEmp" @select-kw="jumpToKw" :studiensemester="selectedStudiensemester"></fhc-coursepicker>
</div>
<stv-studiensemester v-model:studiensemester-kurzbz="selectedStudiensemester"></stv-studiensemester>
</nav>
<tempus-app-menu />
<tempus-sidebar-menu
ref="sidebar"
:preview-role="previewRole"
:rooms="rooms"
:studiengaenge="studiengaenge"
:studiengaenge-all="studiengaenge_all"
:lecturers="lecturers"
:selected-studiensemester="selectedStudiensemester"
@update:preview-role="handlePreviewRoleChange"
@update:rooms="rooms = $event"
@update:studiengaenge="studiengaenge = $event"
@update:parked-keys="parkedKeys = $event"
@update:selected-studiensemester="selectedStudiensemester = $event"
@remove-lecturer="removeLecturer"
@select-lecturer="setEmp"
@select-kw="jumpToKw"
@sync="triggerSync"
/>
<main class="col-md-8 ms-sm-auto col-lg-9 col-xl-10">
<fhc-calendar
ref="calendar"
@@ -1293,16 +953,10 @@ export default {
</div>
</div>
<app-config ref="config" v-model="appconfig" :endpoints="configEndpoints"></app-config>
<div id="verbandMenu" ref="verbandMenu" class="offcanvas offcanvas-start col-md p-md-0 h-100" tabindex="-1" data-cy="verbandMenu">
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<h5 class="offcanvas-title" id="verbandMenuLabel">
<i class="fa-solid fa-university me-2"></i>Verband
</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<stv-verband :endpoint="endpoint" @select-verband="onSelectVerbandAndClose" class="col" style="height:0%"></stv-verband>
</div>
<tempus-verband-menu
:endpoint="endpoint"
@select-verband="onSelectVerband"
/>
<bs-modal ref="raumModal" class="bootstrap-prompt">
<template #title>Raumauswahl</template>
<template #default>
@@ -1321,94 +975,15 @@ export default {
<p v-else class="text-muted mb-0">Keine freien Räume gefunden.</p>
</template>
</bs-modal>
<bs-modal
<resources-assignment-modal
ref="resourcesAssignmentModal"
@hideBsModal="closeResourcesAssignmentModal"
class="bootstrap-prompt"
data-cy="resourcesAssignmentModal"
>
<template #title>{{$p.t('ui', 'resource_assignment_modal_title')}}</template>
<template #default>
<div class="mb-5">
<form-input
v-if="resourcesAssignmentModal.availableResources.length"
@itemSelect="(option) => { resourcesAssignmentModal.selectedAvailableResource = option.value; }"
:label="$p.t('ui', 'available_resources_label')"
:suggestions="resourcesAssignmentModal.filteredAvailableResources"
:optionValue="(option) => option.value"
:optionLabel="(option) => option.label"
@complete="filterAvailableResources"
dropdown
forceSelection
type="autocomplete"
name="availableResources"
:closeOnSelect="false"
>
</form-input>
</div>
<div>
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-2 mx-auto text-bold fw-1">{{$p.t('ui', 'assigned_resources_subtitle')}}</h6>
</div>
<div v-if="resourcesAssignmentModal.assignedResources.length" class="mb-4">
<div
v-for="resource in resourcesAssignmentModal.assignedResources"
:key="resource.betriebsmittel_id"
class=" shadow-sm p-2 mb-2 bg-body rounded"
>
<div class="d-flex justify-content-between align-items-center mb-1">
<p class="m-0">{{ resource.beschreibung }}</p>
<div class="d-flex justify-content-between align-items-center gap-2">
<a href="#" @click.prevent="toggleAssignedResourceNoteInput(resource)" class="ms-auto"><i class="fa fa-edit text-primary"></i></a>
<a href="#" @click.prevent="removeAssignedResource(resource)" class="ms-auto"><i class="fa fa-trash text-danger"></i></a>
</div>
</div>
<form-input
v-if="resource.isNoteTextareaShown"
v-model="resource.anmerkung"
@input="this.resourcesAssignmentModal.areFormButtonsDisplayed = true"
:placeholder="$capitalize($p.t('global/anmerkung'))"
:rows="1"
class="flex-grow-1"
type="textarea"
name="anmerkung"
>
</form-input>
</div>
</div>
<div v-else class="d-flex align-items-center justify-content-center mb-2">
<p class="text-muted mb-0">{{$p.t('ui', 'no_assigned_resources')}}</p>
</div>
<div v-if="resourcesAssignmentModal.areFormButtonsDisplayed" class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-secondary" @click="refreshResourcesAssignmentModalData(resourcesAssignmentModal.calendar)">{{$p.t('ui', 'abbrechen')}}</button>
<button type="button" class="btn btn-primary" @click="saveAssignedResourcesToCalendarItem(resourcesAssignmentModal.calendar, resourcesAssignmentModal.assignedResources)">{{$p.t('ui', 'speichern')}}</button>
</div>
</div>
</template>
</bs-modal>
<bs-modal ref="historyModel" class="bootstrap-prompt" dialogClass="modal-lg" data-cy="historyModal">
<template #title>History</template>
<template #default>
<table v-if="historyEntries.length" class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Von</th>
<th>Bis</th>
<th>Status</th>
<th>Ort</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in historyEntries" :key="entry.id">
<td>{{ entry.von }}</td>
<td>{{ entry.bis }}</td>
<td>{{ entry.status_kurzbz }}</td>
<td>{{ entry.ort }}</td>
</tr>
</tbody>
</table>
</template>
</bs-modal>
@save-finished="$refs.calendar.resetEventLoader()"
/>
<tags-assignment-modal
ref="tagsAssignmentModal"
@tags-changed="$refs.calendar.resetEventLoader()"
/>
<history-modal ref="historyModal" :entries="historyEntries" />
<reservierung
ref="reservierung"
:rooms="rooms"
@@ -0,0 +1,48 @@
import StvVerband from "../Stv/Studentenverwaltung/Verband.js";
export default {
name: "TempusVerbandMenu",
components: {
StvVerband,
},
props: {
endpoint: {
type: Object,
required: true,
},
},
emits: ["select-verband"],
methods: {
onSelectVerband(payload) {
this.$emit("select-verband", payload);
bootstrap.Offcanvas.getOrCreateInstance(this.$refs.menu).hide();
},
},
template: /* html */ `
<div
id="verbandMenu"
ref="menu"
class="offcanvas offcanvas-start col-md p-md-0 h-100"
tabindex="-1"
data-cy="verbandMenu"
>
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<h5 class="offcanvas-title" id="verbandMenuLabel">
<i class="fa-solid fa-university me-2"></i>Verband
</h5>
<button
type="button"
class="btn-close text-reset"
data-bs-dismiss="offcanvas"
:aria-label="$p.t('ui/schliessen')"
></button>
</div>
<stv-verband
:endpoint="endpoint"
class="col"
style="height:0%"
@select-verband="onSelectVerband"
></stv-verband>
</div>
`,
};
@@ -12,6 +12,11 @@ export function useContextMenuActions(handlers)
icon: 'fa-solid fa-table-list',
action: handlers.openResourcesAssignmentModal
},
{
label: 'Tags',
icon: 'fa-solid fa-tags',
action: handlers.openTagsModal
},
{
label: 'Freischalten für Voransicht',
icon: 'fa-solid fa-chalkboard-user',
+1
View File
@@ -100,6 +100,7 @@ require_once('dbupdate_3.4/76150_perm_other_lv_plan.php');
require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php');
require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php');
require_once('dbupdate_3.4/77375_ressourcen.php');
require_once('dbupdate_3.4/78052_tempus_tag_events.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
@@ -0,0 +1,19 @@
<?php
if (! defined('DB_NAME')) exit('No direct script access allowed');
$result = $db->db_query("SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'tbl_notizzuordnung'
AND column_name = 'eindeutige_kalender_gruppen_id'");
if ($db->db_num_rows($result) === 0)
{
$qry = "ALTER TABLE public.tbl_notizzuordnung
ADD COLUMN eindeutige_kalender_gruppen_id UUID NULL DEFAULT NULL";
if (!$db->db_query($qry))
echo '<strong>public.tbl_notizzuordnung: '.$db->db_last_error().'</strong><br>';
else
echo '<br>public.tbl_notizzuordnung: eindeutige_kalender_gruppen_id column added';
}
+80
View File
@@ -61240,6 +61240,26 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'tags_assignment_modal_title',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Tag-Zuweisung',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Tag Assignment',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lvplan',
@@ -61260,6 +61280,66 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'assigned_tags_subtitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Zugewiesene Tags',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Assigned Tags',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'no_assigned_tags',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Keine zugewiesenen Tags',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No Assigned Tags',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'tags',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Tags',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Tags',
'description' => '',
'insertvon' => 'system'
)
)
),
// generic tabulator phrases END
);
+11
View File
@@ -177,6 +177,17 @@ VALUES
(51006, 31),
(51004, 32);
-- Calendar setup for tags tests
INSERT INTO lehre.tbl_kalender
(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, eindeutige_gruppen_id, insertamum, insertvon, updateamum, updatevon)
VALUES
(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '16:55:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '17:40:00', 'lehreinheit', 'live', NULL, 'ec1bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin');
INSERT INTO lehre.tbl_kalender_lehreinheit
(lehreinheit_id, kalender_id)
VALUES
(51004, 33);
-- INSERT INTO public.tbl_variablenname
-- (name, defaultwert)
-- VALUES('ignore_resources_collisions', 'false');
+5
View File
@@ -0,0 +1,5 @@
INSERT INTO public.tbl_notiz_typ
(typ_kurzbz, bezeichnung_mehrsprachig, beschreibung, automatisiert, aktiv, zuordnung, tag, style, vorrueckung, prioritaet/*, taglib*/)
VALUES
('prioone', '{Prio 1,Prio 1}', '', false, true, NULL, false, 'tag_rot', false, 1/*, NULL*/),
('priotwo', '{Prio 2,Prio 2}', '', false, true, NULL, false, 'tag_gelb', false, 2/*, NULL*/);
@@ -71,6 +71,20 @@ context("Tempus smoke tests", () => {
tempusPage.getResourcesModal().should("be.visible");
});
it("shows tags modal when selecting Tags from event context menu", () => {
tempusPage.waitForCalendarToFinishLoading();
tempusPage
.getCalendarEventsByWeekdayAndStartTime("Sunday", "16:55:00")
.should("have.length.greaterThan", 0);
tempusPage.getCalendarEventsByWeekdayAndStartTime("Sunday", "16:55:00").first().rightclick();
tempusPage.getEventContextMenuOption("Tags").click({ force: true });
waitForOk("@fetchTags");
waitForOk("@fetchTagsByCalendar");
tempusPage.getTagsModal().should("be.visible");
});
it("shows history modal when selecting History from event context menu", () => {
tempusPage.waitForCalendarToFinishLoading();
tempusPage.getCalendarEvents().should("have.length.greaterThan", 0);
@@ -72,6 +72,14 @@ class TempusPage {
method: "GET",
url: "**/tempus/OperationalResourceToCalenderAPI/getAssignedResourcesByCalender/**",
}).as("fetchAssignedResources");
cy.intercept({
method: "GET",
url: "**/tempus/Tags/getTags**",
}).as("fetchTags");
cy.intercept({
method: "GET",
url: "**/tempus/Tags/getTagsByCalendar/**",
}).as("fetchTagsByCalendar");
cy.intercept({
method: "POST",
url: "**/tempus/coursepicker/getByStg**",
@@ -345,6 +353,8 @@ class TempusPage {
cy
.get("[data-cy='resourcesAssignmentModal']")
.closest(".bootstrap-modal.show");
getTagsModal = () =>
cy.get("[data-cy='tagsAssignmentModal']").closest(".bootstrap-modal.show")
getCalendarEventRoom = (id) =>
this.getCalendarEventById(id).find("[data-cy='calendar-event-room']");
getStundenrasterToggle = () =>