tempus pre-alpha version

This commit is contained in:
ma0048
2026-03-25 14:41:13 +01:00
parent 43a1d163a3
commit e990bb3d81
41 changed files with 2670 additions and 920 deletions
@@ -1,171 +0,0 @@
<?php
/**
* 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/>.
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Kalender extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getRoomplan' => self::PERM_LOGGED,
'Stunden' => self::PERM_LOGGED,
'Reservierungen' => self::PERM_LOGGED,
'getStundenplan' => self::PERM_LOGGED,
'getLehreinheitStudiensemester' => self::PERM_LOGGED,
'updateKalenderEvent' => 'lehre/lvplan:rw',
'addKalenderEvent' => 'lehre/lvplan:rw'
]);
$this->load->library('LogLib');
$this->loglib->setConfigs(array(
'classIndex' => 5,
'functionIndex' => 5,
'lineIndex' => 4,
'dbLogType' => 'API', // required
'dbExecuteUser' => 'RESTful API'
));
$this->uid = getAuthUID();
$this->load->library('form_validation');
//load models
//$this->load->model('ressource/Stundenplan_model', 'StundenplanModel');
//$this->load->model('ressource/Reservierung_model', 'ReservierungModel');
$this->load->library('KalenderLib');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches Stunden layout from database
* @access public
*
*/
public function Stunden()
{
$this->load->model('ressource/Stunde_model', 'StundeModel');
$stunden = $this->StundeModel->load();
$stunden = $this->getDataOrTerminateWithError($stunden);
$this->terminateWithSuccess($stunden);
}
/**
* fetches stundenplan events from a Room and start/end date
* @access public
*
*/
public function getRoomplan()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('start_date',"start_date","required");
$this->form_validation->set_rules('end_date',"end_date","required");
$this->form_validation->set_rules('ort_kurzbz',"ort_kurzbz","required");
if($this->form_validation->run() === FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the get parameter in local variables
$start_date = $this->input->get('start_date', TRUE);
$end_date = $this->input->get('end_date', TRUE);
$ort_kurzbz = $this->input->get('ort_kurzbz', TRUE);
$stundenplan_data =$this->kalenderlib->getRoomData($ort_kurzbz, $start_date, $end_date);
$this->terminateWithSuccess($stundenplan_data);
}
public function updateKalenderEvent()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('kalender_id',"kalender_id","required");
$this->form_validation->set_rules('ort_kurzbz',"ort_kurzbz","required");
$this->form_validation->set_rules('start_date',"start_date","required");
$this->form_validation->set_rules('end_date',"end_date","optional");
if($this->form_validation->run() === FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the get parameter in local variables
$kalender_id = $this->input->post('kalender_id', TRUE);
$ort_kurzbz = $this->input->post('ort_kurzbz', TRUE);
$start_date = $this->input->post('start_date', TRUE);
$end_date = $this->input->post('end_date', TRUE);
// Was passiert hier?
// Raumänderung, Tagesänderung, Start / Ende Zeit korrektur
// Ist das alles ein Endpunkt?
$stundenplan_data =$this->kalenderlib->updateKalenderEvent($this->uid,$kalender_id, $ort_kurzbz, $start_date, $end_date);
$this->terminateWithSuccess($stundenplan_data);
}
public function addKalenderEvent()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('lehreinheit_id',"kalender_id","required");
$this->form_validation->set_rules('ort_kurzbz',"ort_kurzbz","required");
$this->form_validation->set_rules('start_date',"start_date","required");
$this->form_validation->set_rules('end_date',"end_date","required");
if($this->form_validation->run() === FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the get parameter in local variables
$lehreinheit_id = $this->input->post('lehreinheit_id', TRUE);
$ort_kurzbz = $this->input->post('ort_kurzbz', TRUE);
$start_date = $this->input->post('start_date', TRUE);
$end_date = $this->input->post('end_date', TRUE);
$this->kalenderlib->addKalenderEvent($this->uid, $ort_kurzbz, $start_date, $end_date, $lehreinheit_id);
$this->terminateWithSuccess();
}
// gets the reservierungen of a room if the ort_kurzbz parameter is supplied otherwise gets the reservierungen of the stundenplan of a student
public function Reservierungen($ort_kurzbz = null)
{
$this->terminateWithSuccess();
}
public function getLehreinheitStudiensemester($lehreinheit_id)
{
$this->load->model('education/Lehreinheit_model', 'LehreinheitModel');
$this->LehreinheitModel->addSelect(["studiensemester_kurzbz"]);
$result = $this->LehreinheitModel->load($lehreinheit_id);
$result = current($this->getDataOrTerminateWithError($result))->studiensemester_kurzbz;
$this->terminateWithSuccess($result);
}
}
@@ -1,94 +0,0 @@
<?php
/**
* 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/>.
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Tempus extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getCourses' => self::PERM_LOGGED,
]);
$this->load->library('LogLib');
$this->loglib->setConfigs(array(
'classIndex' => 5,
'functionIndex' => 5,
'lineIndex' => 4,
'dbLogType' => 'API', // required
'dbExecuteUser' => 'RESTful API'
));
$this->load->library('form_validation');
//load models
//$this->load->model('ressource/Stundenplan_model', 'StundenplanModel');
//$this->load->model('ressource/Reservierung_model', 'ReservierungModel');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches courses
* @access public
*
*/
public function getCourses()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('searchfilter',"searchfilter","required");
if($this->form_validation->run() === FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the get parameter in local variables
$searchfilter = $this->input->get('searchfiler', TRUE);
// TODO implement Loading Data
$course_data = array(
array(
'lehreinheit_id'=>'1',
'bezeichnung' => 'Englisch 1',
'studiengang_kurzbz' => 'BMR',
'semester' => '1',
'kurzbz' => 'ENG',
'lektoren' => array('OesterAn','KindlMa')
),
array(
'lehreinheit_id'=>'2',
'bezeichnung' => 'Mahtematik 1',
'studiengang_kurzbz' => 'BMR',
'semester' => '1',
'kurzbz' => 'MAT',
'lektoren' => array('BamberHa')
)
);
$this->terminateWithSuccess($course_data);
}
}
@@ -0,0 +1,70 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class Config extends FHCAPI_Controller
{
private $_ci;
public function __construct()
{
parent::__construct([
'get' => ['admin:r', 'assistenz:r'],
'getHeader' => ['admin:r', 'assistenz:r'],
'set' => ['admin:r', 'assistenz:r'],
]);
// Load Phrases
$this->loadPhrases([
'global',
]);
$this->_ci = &get_instance();
$this->_ci->load->model('ressource/Kalenderstatus_model', 'KalenderStatusModel');
}
public function get()
{
$visible_status = $this->_ci->KalenderStatusModel->load();
$visible_status = getData($visible_status);
$config['visible_status'] = [
"type" => "select",
"label" => $this->p->t('ui', 'status'),
"multiple" => true,
"value" => 'all'
];
foreach ($visible_status as $status)
{
$config['visible_status']['options'][$status->status_kurzbz] = $status->status_kurzbz;
}
$this->terminateWithSuccess($config);
}
public function getHeader()
{
$visible_status = $this->_ci->KalenderStatusModel->load();
$visible_status = getData($visible_status);
$config['visible_status']['all'] = 'all';
foreach ($visible_status as $status)
{
$config['visible_status'][$status->status_kurzbz] = $status->bezeichnung;
}
$this->terminateWithSuccess($config);
}
public function set()
{
$this->terminateWithSuccess();
}
}
@@ -0,0 +1,96 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Coursepicker extends FHCAPI_Controller
{
private $_ci;
public function __construct()
{
parent::__construct([
'search' => self::PERM_LOGGED,
]);
$this->_ci = &get_instance();
$this->load->library('form_validation');
$this->_ci->load->model('education/lehreinheit_model', 'LehreinheitModel');
$this->_ci->load->model('education/Lehreinheitmitarbeiter_model', 'LehreinheitmitarbeiterModel');
$this->loadPhrases([
'ui'
]);
}
public function search()
{
$query = $this->input->get('query');
if (is_null($query))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$query_words = explode(' ', $query);
//TODO Where weiter anpassen z.B. Fachbereich
$this->_ci->LehreinheitModel->addSelect('tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.unr,
tbl_lehreinheit.lvnr,
tbl_lehreinheit.lehrfach_id,
lehrfach.kurzbz AS lehrfach,
lehrfach.bezeichnung AS lehrfach_bez,
lehrfach.farbe AS lehrfach_farbe,
tbl_lehreinheit.lehrform_kurzbz AS lehrform,
lema.mitarbeiter_uid AS lektor_uid,
tbl_mitarbeiter.kurzbz AS lektor,
tbl_studiengang.studiengang_kz,
upper(tbl_studiengang.typ::character varying::text || tbl_studiengang.kurzbz::text) AS studiengang,
lvb.semester,
lvb.verband,
lvb.gruppe,
lvb.gruppe_kurzbz,
tbl_lehreinheit.raumtyp,
tbl_lehreinheit.raumtypalternativ,
tbl_lehreinheit.stundenblockung,
tbl_lehreinheit.wochenrythmus,
lema.semesterstunden,
lema.planstunden,
tbl_lehreinheit.start_kw,
tbl_lehreinheit.anmerkung,
tbl_lehreinheit.studiensemester_kurzbz');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehreinheitmitarbeiter lema', 'tbl_lehreinheit.lehreinheit_id = lema.lehreinheit_id');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehreinheitgruppe lvb', 'tbl_lehreinheit.lehreinheit_id = lvb.lehreinheit_id');
$this->_ci->LehreinheitModel->addJoin('public.tbl_studiengang', 'lvb.studiengang_kz = tbl_studiengang.studiengang_kz');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id');
$this->_ci->LehreinheitModel->addJoin('public.tbl_mitarbeiter', 'lema.mitarbeiter_uid = tbl_mitarbeiter.mitarbeiter_uid');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrform', 'tbl_lehrform.lehrform_kurzbz = tbl_lehreinheit.lehrform_kurzbz');
$this->_ci->MitarbeiterModel->db->group_start();
foreach ($query_words as $word)
{
$this->_ci->LehreinheitModel->db->group_start();
$this->_ci->LehreinheitModel->db->where('lema.mitarbeiter_uid ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lvb.gruppe_kurzbz ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('tbl_studiengang.kurzbzlang ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lvb.verband ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lvb.gruppe ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lehrfach.bezeichnung ILIKE', "%" . $word . "%");
if (is_numeric($word))
{
$this->_ci->LehreinheitModel->db->or_where('tbl_studiengang.studiengang_kz', $word);
$this->_ci->LehreinheitModel->db->or_where('lvb.semester', $word);
}
$this->_ci->LehreinheitModel->db->group_end();
}
$this->_ci->LehreinheitModel->db->group_end();
$this->_ci->LehreinheitModel->db->where('tbl_lehreinheit.studiensemester_kurzbz = \'SS2025\'');
$this->_ci->LehreinheitModel->db->where(array('tbl_lehrform.verplanen' => true));
$result = $this->_ci->LehreinheitModel->load();
$this->terminateWithSuccess(hasData($result) ? getData($result) : array());
}
}
@@ -0,0 +1,303 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Kalender extends FHCAPI_Controller
{
private $_ci;
const ALLOWED_PLAN_FILTER = ['ort', 'uid', 'stg'];
const ALLOWED_ROOM_FILTER = ['lehreinheit_id', 'kalender_id'];
const ALLOWED_TO_UPDATE = ['start_time', 'end_time', 'ort_kurzbz'];
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getStunden' => self::PERM_LOGGED,
'getPlan' => self::PERM_LOGGED,
'getPlanNew' => self::PERM_LOGGED,
'getPlanByOrt' => self::PERM_LOGGED,
'getRaumvorschlag' => self::PERM_LOGGED,
'getZeitwuensche' => self::PERM_LOGGED,
'getZeitsperren' => self::PERM_LOGGED,
'updateKalenderEvent' => 'lehre/lvplan:rw',
'addKalenderEvent' => 'lehre/lvplan:rw'
]);
$this->_ci =& get_instance();
$this->_ci->load->library('LogLib');
$this->_ci->load->library('form_validation');
$this->_ci->load->library('KalenderLib');
$this->loadPhrases([
'ui'
]);
$this->_ci->loglib->setConfigs(array(
'classIndex' => 5,
'functionIndex' => 5,
'lineIndex' => 4,
'dbLogType' => 'API', // required
'dbExecuteUser' => 'RESTful API'
));
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches Stunden layout from database
* @access public
*
*/
public function getStunden()
{
$this->load->model('ressource/Stunde_model', 'StundeModel');
$this->_ci->StundeModel->addOrder('stunde', 'ASC');
$stunden = $this->_ci->StundeModel->load();
$stunden = $this->getDataOrTerminateWithError($stunden);
$this->terminateWithSuccess($stunden);
}
public function getPlan()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER);
$stundenplan_data = $this->_ci->kalenderlib->getPlan(
$start_date,
$end_date,
isset($filter->ort) ? $filter->ort : null,
isset($filter->uid) ? $filter->uid : null,
isset($filter->stg) ? $filter->stg : null
);
$this->terminateWithSuccess($stundenplan_data);
}
public function getPlanNew()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER);
$stundenplan_data = $this->_ci->kalenderlib->getPlanNew(
$start_date,
$end_date,
isset($filter->ort) ? $filter->ort : null,
isset($filter->uid) ? $filter->uid : null,
isset($filter->stg) ? $filter->stg : null
);
$this->terminateWithSuccess($stundenplan_data);
}
public function getPlanByOrt($start_date = null, $end_date = null, $ort = null)
{
if (!isset($start_date) || !isset($end_date) || !isset($ort))
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
$this->_ci->form_validation->set_rules('ort',"ort","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$ort = $this->_ci->input->get('ort', TRUE);
}
$this->terminateWithSuccess($this->_ci->kalenderlib->getPlanByOrt($start_date, $end_date, $ort));
}
public function getZeitsperren()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
$this->_ci->form_validation->set_rules('emp',"emp","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$emp = $this->_ci->input->get('emp', TRUE);
$stundenplan_data = $this->_ci->kalenderlib->getZeitsperren($start_date, $end_date, $emp);
$this->terminateWithSuccess($stundenplan_data);
}
public function getZeitwuensche()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
$this->_ci->form_validation->set_rules('emp',"emp","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$emp = $this->_ci->input->get('emp', TRUE);
$stundenplan_data = $this->_ci->kalenderlib->getZeitwuensche($start_date, $end_date, $emp);
$this->terminateWithSuccess($stundenplan_data);
}
public function updateKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id',"kalender_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$updateFields = $this->_checkUpdate($this->_ci->input->post('updatedInfos', TRUE));
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
if (isset($updateFields->ort_kurzbz) && !isEmptyString($updateFields->ort_kurzbz))
{
$result = $this->_ci->kalenderlib->updateOrt($kalender_id, $updateFields->ort_kurzbz);
if (isError($result))
$this->terminateWithError(getError($result));
}
if (isset($updateFields->start_time) || isset($updateFields->end_time))
{
$result = $this->_ci->kalenderlib->updateZeit($kalender_id, $updateFields->start_time, $updateFields->end_time);
if (isError($result))
$this->terminateWithError(getError($result));
}
$this->terminateWithSuccess('Erfolgreich');
}
public function getRaumvorschlag()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$filter = $this->_checkFilter(self::ALLOWED_ROOM_FILTER);
if (isset($filter->lehreinheit_id))
{
$result = $this->_ci->kalenderlib->getRaumvorschlagByLehreinheitID(
$start_date,
$end_date,
$filter->lehreinheit_id
);
}
if (isset($filter->kalender_id))
{
$result = $this->_ci->kalenderlib->getRaumvorschlagByKalenderID(
$start_date,
$end_date,
$filter->kalender_id
);
}
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function addKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE);
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$result = $this->_ci->kalenderlib->addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess('Erfolgreich');
}
private function _checkFilter($filters)
{
$filter_valid = true;
$filter_object = new stdClass();
foreach ($filters as $filter)
{
if ($this->_ci->input->get($filter))
{
$filter_valid = true;
$filter_object->$filter = $this->_ci->input->get($filter);
}
}
if (!$filter_valid)
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
return $filter_object;
}
private function _checkUpdate($updateInfos)
{
$update_valid = false;
$update_object = new stdClass();
foreach (self::ALLOWED_TO_UPDATE as $filter)
{
if (isset($updateInfos[$filter]))
{
$update_valid = true;
$update_object->$filter = $updateInfos[$filter];
}
}
if (!$update_valid)
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
return $update_object;
}
}
@@ -8,7 +8,7 @@
if (! defined('BASEPATH')) exit('No direct script access allowed');
class MigrateKalender extends CLI_Controller
class MigrateKalender extends Auth_Controller
{
/**
@@ -16,7 +16,7 @@ class MigrateKalender extends CLI_Controller
*/
public function __construct()
{
parent::__construct();
parent::__construct(array('index' => ['admin:rw']));
$this->load->model('ressource/Kalender_model', 'KalenderModel');
$this->load->model('ressource/Kalender_Lehreinheit_model', 'KalenderLehreinheitModel');
@@ -27,21 +27,65 @@ class MigrateKalender extends CLI_Controller
/**
* Everything has a beginning
*/
public function index()
public function index($von = null, $bis = null)
{
$von = date('Y-m-d') // TODO
$bis = date('Y-m-d') // TODO
$db = new DB_Model();
$stpldevsql = '
SELECT *,
(SELECT beginn FROM lehre.tbl_stunde WHERE stunde=tbl_stundenplandev.stunde) as beginn,
(SELECT ende FROM lehre.tbl_stunde WHERE stunde=tbl_stundenplandev.stunde) as ende
FROM
lehre.tbl_stundenplandev WHERE datum>=? and datum<=? ORDER BY datum, stunde, unr';
$stpldev = $db->execReadOnlyQuery($stpldevsql, array($von, $bis));
if (hasData($stpldev))
$stpldevsql = '
WITH eindeutige_stunden AS (
SELECT DISTINCT unr, datum, stunde
FROM lehre.tbl_stundenplandev
WHERE datum >= ? AND datum <= ?
),
block_keys AS (
SELECT
unr,
datum,
stunde,
stunde - ROW_NUMBER() OVER (PARTITION BY unr, datum ORDER BY stunde) AS block_nr
FROM eindeutige_stunden
),
blocks AS (
SELECT
bk.unr,
bk.datum,
bk.block_nr,
MIN(bk.stunde) AS stunde_von,
MAX(bk.stunde) AS stunde_bis,
MIN(sp.lehreinheit_id) AS lehreinheit_id,
MIN(sp.ort_kurzbz) AS ort_kurzbz,
array_agg(sp.stundenplandev_id ORDER BY bk.stunde) AS stundenplandev_ids,
MIN(sp.insertamum) AS insertamum,
(array_agg(sp.insertvon ORDER BY sp.insertamum ASC))[1] AS insertvon,
MAX(sp.updateamum) AS updateamum,
(array_agg(sp.updatevon ORDER BY sp.updateamum DESC))[1] AS updatevon
FROM block_keys bk JOIN lehre.tbl_stundenplandev sp ON sp.unr = bk.unr AND sp.datum = bk.datum AND sp.stunde = bk.stunde
WHERE sp.datum >= ? AND sp.datum <= ?
GROUP BY bk.unr, bk.datum, bk.block_nr
)
SELECT
b.stundenplandev_ids,
b.unr,
b.datum,
b.block_nr,
b.lehreinheit_id,
b.ort_kurzbz,
b.datum + stundevon.beginn AS von,
b.datum + stundebis.ende AS bis,
b.insertamum,
b.insertvon,
b.updateamum,
b.updatevon
FROM blocks b
JOIN lehre.tbl_stunde stundevon ON stundevon.stunde = b.stunde_von
JOIN lehre.tbl_stunde stundebis ON stundebis.stunde = b.stunde_bis
ORDER BY b.datum, b.unr, b.block_nr;';
$stpldev = $db->execReadOnlyQuery($stpldevsql, array($von, $bis, $von, $bis));
if (hasData($stpldev))
{
// Pruefen ob der Eintrag schon in Sync Tabelle vorhanden ist
// Wenn neuere Änderungen vorhanden dann Update
@@ -51,12 +95,39 @@ class MigrateKalender extends CLI_Controller
// in der neuen Tabelle auch archivieren oder loeschen
$data = getData($stpldev);
foreach($data as $rowstpl)
foreach($data as $block)
{
$SyncResult = $this->SyncModel->loadWhere(
array('stundenplandev_id' => $rowstpl->stundenplandev_id)
);
if(hasData($SyncResult))
$this->SyncModel->db->where_in('stundenplandev_id', $block->stundenplandev_ids);
$sync_result = $this->SyncModel->load();
if (!hasData($sync_result))
{
$kalender_id = $this->_insertKalender($block);
if ($kalender_id)
{
$this->_insertSync($block->stundenplandev_ids, $kalender_id);
}
}
else
{
$syncData = getData($sync_result);
$kalender_id = $syncData[0]->kalender_id;
$last_sync = $syncData[0]->lastupdate;
$synced_ids = array_column($syncData, 'stundenplandev_id');
if ($block->updateamum > $last_sync)
{
$this->_updateKalender($kalender_id, $block);
$this->_updateSync($synced_ids, $kalender_id);
}
$fehlende = array_diff($block->stundenplandev_ids, $synced_ids);
if (!empty($fehlende))
{
$this->_insertSync($fehlende, $kalender_id);
}
}
/*if(hasData($SyncResult))
{
//bereits vorhanden
// TODO Update
@@ -115,8 +186,89 @@ class MigrateKalender extends CLI_Controller
);
}
}
}*/
}
}
}
private function _insertKalender($block)
{
$result = $this->KalenderModel->insert(
array (
'von' => $block->von,
'bis' => $block->bis,
'typ' => 'lehreinheit',
'status_kurzbz'=> 'visible_student',
'insertamum' => $block->insertamum,
'insertvon' => $block->insertvon,
'updateamum' => $block->updateamum,
'updatevon' => $block->updatevon
)
);
if(!isSuccess($result))
return null;
$kalender_id = getData($result);
$this->KalenderLehreinheitModel->insert(
array (
'kalender_id' => $kalender_id,
'lehreinheit_id'=> $block->lehreinheit_id
)
);
$this->KalenderOrtModel->insert(
array (
'kalender_id' => $kalender_id,
'ort_kurzbz' => $block->ort_kurzbz
)
);
return $kalender_id;
}
private function _insertSync($ids, $kalender_id)
{
foreach($ids as $id)
{
$this->SyncModel->insert(
array (
'stundenplandev_id' => $id,
'kalender_id' => $kalender_id,
'lastupdate' => date('Y-m-d H:i:s')
)
);
}
}
private function _updateKalender($kalender_id, $block)
{
$this->KalenderModel->update(
array (
'kalender_id' => $kalender_id
),
array (
'von' => $block->von,
'bis' => $block->bis,
'updateamum'=> $block->updateamum,
'updatevon' => $block->updatevon
)
);
}
private function _updateSync($ids, $kalender_id)
{
foreach($ids as $id)
{
$this->SyncModel->update(
array (
'stundenplandev_id' => $id,
'kalender_id' => $kalender_id
),
array (
'lastupdate' => date('Y-m-d H:i:s')
)
);
}
}
}
+446 -123
View File
@@ -4,159 +4,480 @@ if (! defined("BASEPATH")) exit("No direct script access allowed");
class KalenderLib
{
private $_ci;
/**
* Loads model OrganisationseinheitModel
*/
public function __construct()
{
$this->ci =& get_instance();
$this->_ci =& get_instance();
$this->ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->ci->load->model('ressource/Kalender_Lehreinheit_model', 'KalenderLehreinheitModel');
$this->ci->load->model('ressource/Kalender_Ort_model', 'KalenderOrtModel');
$this->ci->load->model('education/Lehreinheit_model', 'LehreinheitModel');
$this->ci->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$this->ci->load->model('education/LehreinheitMitarbeiter_model', 'LehreinheitMitarbeiterModel');
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->model('ressource/Kalender_Lehreinheit_model', 'KalenderLehreinheitModel');
$this->_ci->load->model('ressource/Kalender_Ort_model', 'KalenderOrtModel');
$this->_ci->load->model('education/Lehreinheit_model', 'LehreinheitModel');
$this->_ci->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$this->_ci->load->model('education/LehreinheitMitarbeiter_model', 'LehreinheitMitarbeiterModel');
$this->_ci->load->model('ressource/Ort_model', 'OrtModel');
}
public function getRoomData($ort_kurzbz, $start_date, $end_date)
private function _getBasePlan($start_date, $end_date)
{
$data = $this->ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'kalender_id');
$data = $this->ci->KalenderModel->loadWhere(array(
'von >=' => $start_date,
'bis <= '=>$end_date,
'ort_kurzbz'=>$ort_kurzbz
));
$end_date = date('Y-m-d', strtotime($end_date . ' +1 day'));
$stundenplan_data = array();
if(isSuccess($data) && hasData($data))
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id,
tbl_kalender.status_kurzbz,
tbl_kalender.von,
tbl_kalender.bis,
tbl_kalender_ort.ort_kurzbz,
tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.lehrveranstaltung_id,
tbl_lehreinheit.lehrfach_id,
tbl_lehreinheit.lehrform_kurzbz,
tbl_lehrveranstaltung.oe_kurzbz,
lehrfach.kurzbz AS lehrfach_kurzbz,
lehrfach.bezeichnung AS lehrfach_bezeichnung,
lehrfach.farbe,
tbl_lehreinheitmitarbeiter.mitarbeiter_uid,
tbl_person.vorname,
tbl_paddKalenderEventerson.nachname,
tbl_mitarbeiter.kurzbz AS ma_kurzbz');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit', 'tbl_kalender_lehreinheit.lehreinheit_id = tbl_lehreinheit.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung', 'tbl_lehreinheit.lehrveranstaltung_id = tbl_lehrveranstaltung.lehrveranstaltung_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter', 'tbl_lehreinheit.lehreinheit_id = tbl_lehreinheitmitarbeiter.lehreinheit_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_mitarbeiter', 'tbl_mitarbeiter.mitarbeiter_uid = tbl_lehreinheitmitarbeiter.mitarbeiter_uid', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_benutzer', 'tbl_mitarbeiter.mitarbeiter_uid = tbl_benutzer.uid', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_person', 'tbl_person.person_id = tbl_benutzer.person_id', 'LEFT');
$this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date);
$this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date);
}
private function _mapEvents($data)
{
$stundenplan_data = [];
if (!isSuccess($data) || !hasData($data))
return $stundenplan_data;
$events = [];
foreach (getData($data) as $row)
{
$data = getData($data);
foreach($data as $rowstpl)
$id = $row->kalender_id;
if (!isset($events[$id]))
{
$obj = new stdClass();
$obj->type='lehreinheit';
$von = new DateTime($row->von);
$bis = new DateTime($row->bis);
$von = new DateTime($rowstpl->von);
$bis = new DateTime($rowstpl->bis);
$events[$id] = (object) [
'type' => 'lehreinheit',
'beginn' => $von->format('H:i:s'),
'ende' => $bis->format('H:i:s'),
'datum' => $von->format('Y-m-d'),
'isostart' => $von->format('c'),
'isoend' => $bis->format('c'),
'tooltip' => 'tip',
'status_kurzbz' => $row->status_kurzbz,
'ort_kurzbz' => isset($row->ort_kurzbz) ? $row->ort_kurzbz : '',
'lehrform' => isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : '',
'lehrfach' => isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '',
'lehrfach_bez' => isset($row->lehrfach_bezeichnung) ? $row->lehrfach_bezeichnung : '',
'farbe' => isset($row->farbe) ? $row->farbe : '',
'lehrveranstaltung_id' => $row->lehrveranstaltung_id,
'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '',
'kalender_id' => $id,
'lehreinheit_id' => [],
'lektor' => [],
'gruppe' => [],
'titel' => '',
'topic' => (isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '').' '.(isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : ''),
];
}
$obj->beginn = $von->format('H:i:s');
$obj->ende = $bis->format('H:i:s');
$obj->datum = $von->format('Y-m-d');
$obj->topic = 'undefined';
$obj->lektor = array();
$obj->gruppe = array();
$obj->isostart = $von->format('c');
$obj->isoend = $bis->format('c');
$obj->tooltip = 'tip';
if ($row->lehreinheit_id && !in_array($row->lehreinheit_id, $events[$id]->lehreinheit_id))
$events[$id]->lehreinheit_id[] = $row->lehreinheit_id;
$obj->lehreinheit_id = array();
$lehreinheiten = $this->ci->KalenderLehreinheitModel->loadWhere(array('kalender_id'=>$rowstpl->kalender_id));
if(isSuccess($lehreinheiten) && hasData($lehreinheiten))
if ($row->mitarbeiter_uid)
{
if (!in_array($row->mitarbeiter_uid, array_column($events[$id]->lektor, 'mitarbeiter_uid')))
{
$lehreinheiten = getData($lehreinheiten);
foreach($lehreinheiten as $le)
{
$obj->lehreinheit_id[] = $le->lehreinheit_id;
$lehreinheitdata = $this->ci->LehreinheitModel->loadWhere(array('lehreinheit_id'=>$le->lehreinheit_id));
if(isSuccess($lehreinheitdata) && hasData($lehreinheitdata))
{
$ledata = getData($lehreinheitdata)[0];
$lvid = $ledata->lehrveranstaltung_id;
$lehrfach_id = $ledata->lehrfach_id;
$obj->lehrform = $ledata->lehrform_kurzbz;
$lehreinheitmitarbeiterdata = $this->ci->LehreinheitMitarbeiterModel->loadWhere(array('lehreinheit_id'=>$le->lehreinheit_id));
$lemitarbeiterdata = getData($lehreinheitmitarbeiterdata);
foreach($lemitarbeiterdata as $rowma)
{
$obj->lektor[] = array(
"mitarbeiter_uid"=> $rowma->mitarbeiter_uid,
"vorname"=>$rowma->mitarbeiter_uid,
"nachname"=>$rowma->mitarbeiter_uid,
"kurzbz"=>$rowma->mitarbeiter_uid
);
}
}
else
{
// TODO
}
}
$events[$id]->lektor[] = [
'mitarbeiter_uid' => $row->mitarbeiter_uid,
'vorname' => $row->vorname,
'nachname' => $row->nachname,
'kurzbz' => $row->ma_kurzbz,
];
}
$lehrfachdata = $this->ci->LehrveranstaltungModel->loadWhere(array('lehrveranstaltung_id' => $lehrfach_id));
$lfdata = getData($lehrfachdata)[0];
$lehrveranstaltungdata = $this->ci->LehrveranstaltungModel->loadWhere(array('lehrveranstaltung_id' => $lvid));
$lvdata = getData($lehrveranstaltungdata)[0];
$obj->topic = $lfdata->kurzbz.' '.$obj->lehrform;
$orte = $this->ci->KalenderOrtModel->loadWhere(array('kalender_id'=>$rowstpl->kalender_id));
$obj->ort_kurzbz = '';
if(isSuccess($orte) && hasData($orte))
{
$ortedata = getdata($orte);
foreach($ortedata as $ort);
{
$obj->ort_kurzbz .= $ort->ort_kurzbz;
}
}
$obj->titel = '';
$obj->lehrfach = $lfdata->kurzbz;
$obj->lehrfach_bez = $lfdata->bezeichnung;
$obj->organisationseinheit = $lvdata->oe_kurzbz;
$obj->farbe = $lfdata->farbe;
$obj->lehrveranstaltung_id = $lvid;
$obj->kalender_id = $rowstpl->kalender_id;
$stundenplan_data[] = $obj;
}
}
return $stundenplan_data;
return array_values($events);
}
public function getPlanByOrt($start_date, $end_date, $ort)
{
$this->_getBasePlan($start_date, $end_date);
$this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort);
$data = $this->_ci->KalenderModel->load();
return $this->_mapEvents($data);
}
public function addKalenderEvent($user, $ort_kurzbz, $start_date, $end_date, $lehreinheit_id)
public function getRaumvorschlagByLehreinheitID($start_date, $end_date, $lehreinheit_id)
{
$kalenderresult = $this->ci->KalenderModel->insert(array(
'von' => $start_date,
'bis' => $end_date,
'typ' => 'lehreinheit',
'status_kurzbz' => 'planning',
'insertvon' => $user,
'insertamum' => date('Y-m-d H:i:s')
));
$end_date = date('Y-m-d', strtotime($end_date . ' +1 day'));
$lehreinheit = $this->_ci->LehreinheitModel->load(array('lehreinheit_id' => $lehreinheit_id));
if (isError($lehreinheit))
return $lehreinheit;
if (!hasData($lehreinheit))
return error("Not found");
$lehreinheit = getData($lehreinheit)[0];
$this->_ci->KalenderModel->addDistinct('ort_kurzbz');
$this->_ci->KalenderModel->addSelect('ort_kurzbz');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'kalender_id');
$this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date);
$this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date);
$belegte_raeume = $this->_ci->KalenderModel->load();
if (isError($belegte_raeume))
return $belegte_raeume;
$belegte_orte = hasData($belegte_raeume) ? array_column(getData($belegte_raeume), 'ort_kurzbz') : [];
$vorschlaege = $this->_getFreieRaeume($lehreinheit->raumtyp, $belegte_orte);
if (isError($vorschlaege))
return $vorschlaege;
$vorschlaege = hasData($vorschlaege) ? getData($vorschlaege) : [];
if (count($vorschlaege) < 5 && !empty($lehreinheit->raumtypalternativ))
{
$bereits_gefunden = array_merge($belegte_orte, array_column($vorschlaege, 'ort_kurzbz'));
$alternativ_raeume = $this->_getFreieRaeume($lehreinheit->raumtypalternativ, $bereits_gefunden);
if (!isError($alternativ_raeume) && hasData($alternativ_raeume))
$vorschlaege = array_merge($vorschlaege, getData($alternativ_raeume));
}
return success($vorschlaege);
}
private function _getFreieRaeume($raumtyp, $belegte_orte)
{
$this->_ci->OrtModel->addSelect('ort_kurzbz');
$this->_ci->OrtModel->addJoin('public.tbl_ortraumtyp', 'ort_kurzbz');
$this->_ci->OrtModel->db->where('raumtyp_kurzbz', $raumtyp);
$this->_ci->OrtModel->db->where('aktiv', true);
$this->_ci->OrtModel->db->where("ort_kurzbz NOT LIKE '\_%'", null, false);
if (!empty($belegte_orte))
$this->_ci->OrtModel->db->where_not_in('ort_kurzbz', $belegte_orte);
$this->_ci->OrtModel->addOrder('hierarchie, ort_kurzbz');
return $this->_ci->OrtModel->load();
}
public function getPlanNew($start_date, $end_date, $ort = null, $uids = null, $stg = null)
{
$this->_getBasePlan($start_date, $end_date);
if (!is_null($ort))
$this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort);
if (!is_null($uids))
$this->_ci->KalenderModel->db->where_in('tbl_lehreinheitmitarbeiter.mitarbeiter_uid', $uids);
if (!is_null($stg))
$this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $stg);
$data = $this->_ci->KalenderModel->load();
return $this->_mapEvents($data);
}
public function getPlan($start_date, $end_date, $ort = null, $uids = null, $stg = null)
{
$end_date = date('Y-m-d', strtotime($end_date . ' +1 day'));
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id,
tbl_kalender.status_kurzbz,
tbl_kalender.von,
tbl_kalender.bis,
tbl_kalender_ort.ort_kurzbz,
tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.lehrveranstaltung_id,
tbl_lehreinheit.lehrfach_id,
tbl_lehreinheit.lehrform_kurzbz,
tbl_lehrveranstaltung.oe_kurzbz,
lehrfach.kurzbz AS lehrfach_kurzbz,
lehrfach.bezeichnung AS lehrfach_bezeichnung,
lehrfach.farbe,
tbl_lehreinheitmitarbeiter.mitarbeiter_uid,
tbl_person.vorname,
tbl_person.nachname,
tbl_mitarbeiter.kurzbz AS ma_kurzbz');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit', 'tbl_kalender_lehreinheit.lehreinheit_id = tbl_lehreinheit.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung', 'tbl_lehreinheit.lehrveranstaltung_id = tbl_lehrveranstaltung.lehrveranstaltung_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter', 'tbl_lehreinheit.lehreinheit_id = tbl_lehreinheitmitarbeiter.lehreinheit_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_mitarbeiter', 'tbl_mitarbeiter.mitarbeiter_uid = tbl_lehreinheitmitarbeiter.mitarbeiter_uid', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_benutzer', 'tbl_mitarbeiter.mitarbeiter_uid = tbl_benutzer.uid', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_person', 'tbl_person.person_id = tbl_benutzer.person_id', 'LEFT');
$this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date);
$this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date);
if (!is_null($ort))
$this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort);
if (!is_null($uids))
$this->_ci->KalenderModel->db->where_in('tbl_lehreinheitmitarbeiter.mitarbeiter_uid', $uids);
if (!is_null($stg))
$this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $stg);
$data = $this->_ci->KalenderModel->load();
$stundenplan_data = [];
if (!isSuccess($data) || !hasData($data))
return $stundenplan_data;
$events = [];
foreach (getData($data) as $row)
{
$id = $row->kalender_id;
if (!isset($events[$id]))
{
$von = new DateTime($row->von);
$bis = new DateTime($row->bis);
$events[$id] = (object) [
'type' => 'lehreinheit',
'beginn' => $von->format('H:i:s'),
'ende' => $bis->format('H:i:s'),
'datum' => $von->format('Y-m-d'),
'isostart' => $von->format('c'),
'isoend' => $bis->format('c'),
'tooltip' => 'tip',
'status_kurzbz' => $row->status_kurzbz,
'ort_kurzbz' => isset($row->ort_kurzbz) ? $row->ort_kurzbz : '',
'lehrform' => isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : '',
'lehrfach' => isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '',
'lehrfach_bez' => isset($row->lehrfach_bezeichnung) ? $row->lehrfach_bezeichnung : '',
'farbe' => isset($row->farbe) ? $row->farbe : '',
'lehrveranstaltung_id' => $row->lehrveranstaltung_id,
'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '',
'kalender_id' => $id,
'lehreinheit_id' => [],
'lektor' => [],
'gruppe' => [],
'titel' => '',
'topic' => (isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '') . ' ' . (isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : ''),
];
}
if ($row->lehreinheit_id && !in_array($row->lehreinheit_id, $events[$id]->lehreinheit_id))
$events[$id]->lehreinheit_id[] = $row->lehreinheit_id;
if ($row->mitarbeiter_uid)
{
if (!in_array($row->mitarbeiter_uid, array_column($events[$id]->lektor, 'mitarbeiter_uid')))
{
$events[$id]->lektor[] = [
'mitarbeiter_uid' => $row->mitarbeiter_uid,
'vorname' => $row->vorname,
'nachname' => $row->nachname,
'kurzbz' => $row->ma_kurzbz,
];
}
}
}
return array_values($events);
}
public function getZeitsperren($start_date, $end_date, $emp)
{
$db = new DB_Model();
$qry = "
SELECT
tbl_zeitsperre.zeitsperre_id,
tbl_zeitsperre.vondatum AS start,
tbl_zeitsperre.bisdatum AS ende,
tbl_vonstunde.beginn AS startstunde,
tbl_bisstunde.ende AS bisstunde,
tbl_erreichbarkeit.farbe AS erreichbarkeit_farbe,
tbl_erreichbarkeit.beschreibung AS erreichbarkeit_beschreibung,
tbl_zeitsperretyp.beschreibung as label
FROM campus.tbl_zeitsperre
JOIN campus.tbl_zeitsperretyp ON tbl_zeitsperre.zeitsperretyp_kurzbz = tbl_zeitsperretyp.zeitsperretyp_kurzbz
LEFT JOIN campus.tbl_erreichbarkeit ON tbl_zeitsperre.erreichbarkeit_kurzbz = tbl_erreichbarkeit.erreichbarkeit_kurzbz
LEFT JOIN lehre.tbl_stunde tbl_vonstunde ON tbl_zeitsperre.vonstunde = tbl_vonstunde.stunde
LEFT JOIN lehre.tbl_stunde tbl_bisstunde ON tbl_zeitsperre.bisstunde = tbl_bisstunde.stunde
WHERE tbl_zeitsperre.mitarbeiter_uid = ?
AND tbl_zeitsperre.bisdatum >= ?
AND tbl_zeitsperre.vondatum <= ?
ORDER BY tbl_zeitsperre.vondatum;
";
$result = $db->execReadOnlyQuery($qry, array($emp, $start_date, $end_date));
if (isError($result))
return $result;
$zeitsperren_array = array();
if (hasData($result))
{
foreach (getData($result) as $zeitsperre)
{
$obj = new stdClass();
$von = new DateTime($zeitsperre->start . ' '. $zeitsperre->startstunde);
$bis = new DateTime($zeitsperre->ende . ' '. $zeitsperre->bisstunde);
$obj->isostart = $von->format('c');
$obj->isoend = $bis->format('c');
$obj->label = $zeitsperre->label;
$zeitsperren_array[] = $obj;
}
}
return $zeitsperren_array;
}
public function getZeitwuensche($start_date, $end_date, $emp)
{
$db = new DB_Model();
$qry = "
WITH zeitwuensche AS (
SELECT
tbl_zeitwunsch.*,
zw_gueltigkeit.von AS gueltig_von,
zw_gueltigkeit.bis AS gueltig_bis
FROM campus.tbl_zeitwunsch
JOIN campus.tbl_zeitwunsch_gueltigkeit zw_gueltigkeit USING (zeitwunsch_gueltigkeit_id)
WHERE tbl_zeitwunsch.mitarbeiter_uid = ?
),
tage AS (
SELECT
tage::date AS tag,
EXTRACT(DOW FROM tage)::int AS wochentag
FROM generate_series(?::date, ?::date, interval '1 day') AS tage
)
SELECT
tage.tag,
zeitwuensche.gewicht,
tage.tag + s.beginn as start,
tage.tag + s.ende as ende,
mitarbeiter_uid as label
FROM tage
JOIN zeitwuensche ON tage.wochentag = zeitwuensche.tag
AND tage.tag >= zeitwuensche.gueltig_von
AND (zeitwuensche.gueltig_bis IS NULL OR tage.tag <= zeitwuensche.gueltig_bis)
JOIN lehre.tbl_stunde s ON s.stunde = zeitwuensche.stunde
ORDER BY tage.tag, start;";
$result = $db->execReadOnlyQuery($qry, array($emp, $start_date, $end_date));
if (isError($result))
return $result;
$zeitwuensche_array = array();
if (hasData($result))
{
foreach (getData($result) as $zeitwuensch)
{
$obj = new stdClass();
$von = new DateTime($zeitwuensch->start);
$bis = new DateTime($zeitwuensch->ende);
$obj->isostart = $von->format('c');
$obj->isoend = $bis->format('c');
$obj->gewicht = $zeitwuensch->gewicht;
$obj->label = $zeitwuensch->label;
$zeitwuensche_array[] = $obj;
}
}
return $zeitwuensche_array;
}
public function addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz)
{
$kalenderresult = $this->_ci->KalenderModel->insert(
array (
'von' => $start_date,
'bis' => $end_date,
'typ' => 'lehreinheit',
'status_kurzbz' => 'planning',
'insertvon' => getAuthUID(),
'insertamum' => date('Y-m-d H:i:s')
)
);
if(isSuccess($kalenderresult) && hasData($kalenderresult))
{
$kalender_id = getData($kalenderresult);
$kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert(
array (
'kalender_id' => $kalender_id,
'lehreinheit_id' => $lehreinheit_id
)
);
$kalenderlehreinheitresult = $this->ci->KalenderLehreinheitModel->insert(array(
'kalender_id' => $kalender_id,
'lehreinheit_id' => $lehreinheit_id
));
if(isSuccess($kalenderlehreinheitresult))
if(isSuccess($kalenderlehreinheitresult) && !is_null($ort_kurzbz))
{
$kalenderOrtresult = $this->ci->KalenderOrtModel->insert(array(
'kalender_id'=>$kalender_id,
'ort_kurzbz'=>$ort_kurzbz
));
return $this->_addKalenderOrt($kalender_id, $ort_kurzbz);
}
return $kalenderlehreinheitresult;
}
}
public function updateKalenderEvent($user, $kalender_id, $ort_kurzbz, $start_date, $end_date)
private function _addKalenderOrt($kalender_id, $ort_kurzbz)
{
return $this->_ci->KalenderOrtModel->insert(
array (
'kalender_id'=>$kalender_id,
'ort_kurzbz'=>$ort_kurzbz
)
);
}
public function updateOrt($kalender_id, $ort_kurzbz)
{
$exist = $this->_ci->KalenderOrtModel->load(array('kalender_id' => $kalender_id));
if (hasData($exist))
{
return $this->_ci->KalenderOrtModel->update(array('kalender_id' => $kalender_id),
array (
'ort_kurzbz' => $ort_kurzbz
)
);
}
else
{
return $this->_addKalenderOrt($kalender_id, $ort_kurzbz);
}
}
public function updateZeit($kalender_id, $start_date, $end_date)
{
/*TODO Checks:
Von-Tag muss gleich dem Bis-Tag sein
@@ -165,13 +486,15 @@ class KalenderLib
History erstellen
Sync Status setzen
*/
$this->ci->KalenderModel->update($kalender_id,
array(
'von'=>$start_date,
'updateamum'=>date('Y-m-d H:i:s'),
'updatevon' => $user
$this->_ci->KalenderModel->update($kalender_id,
array (
'von' => $start_date,
'bis' => $end_date,
'updateamum'=> date('Y-m-d H:i:s'),
'updatevon' => getAuthUID()
)
);
return success();
}
}
@@ -0,0 +1,14 @@
<?php
class Kalenderstatus_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_status';
$this->pk = 'status_kurzbz';
}
}
+6 -2
View File
@@ -5,9 +5,9 @@
'bootstrap5' => true,
'fontawesome6' => true,
'vue3' => true,
'vuedatepicker11' => true,
'primevue3' => true,
'tabulator5' => true,
'vuedatepicker11' => true,
'phrases' => array(
'global',
'ui',
@@ -17,7 +17,9 @@
'public/css/components/vue-datepicker.css',
'public/css/components/primevue.css',
'public/css/components/calendar.css',
'public/css/Tempus.css'
'public/css/Tempus.css',
'public/css/Studentenverwaltung.css',
'public/css/components/function.css'
],
'customJSs' => [
#'vendor/npm-asset/primevue/tree/tree.min.js',
@@ -37,6 +39,8 @@
active-addons="<?= defined('ACTIVE_ADDONS') ? ACTIVE_ADDONS : ''; ?>"
tempus-root="<?= site_url('Tempus'); ?>"
cis-root="<?= CIS_ROOT; ?>"
avatar-url="<?= site_url('Cis/Pub/bild/person/' . getAuthPersonId()); ?>"
logout-url="<?= site_url('Cis/Auth/logout'); ?>"
:permissions="<?= htmlspecialchars(json_encode($permissions)); ?>"
:config="<?= htmlspecialchars(json_encode($variables)); ?>"
>
+115 -35
View File
@@ -77,48 +77,26 @@ body {
color: var(--bs-success);
}
#parkinglot {
#parkingslot {
border: 1px dashed;
width: 300px;
height: 100px;
max-height: 400px;
color: #AAAAAA;
text-align: center;
font-size: large;
margin-top: 20px;
padding-top: 0.5rem;
}
#coursechooser {
width: 300px;
min-height: 100px;
font-size: large;
margin-top: 50px;
border: 1px solid #ccc;
background: #eee;
text-align: left;
padding-left: 15px;
}
#coursechooserheader {
font-weight: bold;
font-size: medium;
}
#coursechooserfooter {
font-size: small;
color: #AAAAAA;
}
.eckerl {
width: 200px;
margin: 10px 0;
padding: 2px 4px;
background: #00649c;
color: #fff;
font-size: .85em;
cursor: pointer;
border-radius: 2px;
box-shadow: 3px 3px 3px #bbb;
.parkingevent {
border: 1px dashed !important;
border-color: #AAAAAA;
padding: 0.5rem;
margin-bottom: 0.5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
:root{
@@ -128,3 +106,105 @@ body {
.eckerltest {
box-shadow: 3px 3px 3px #ccc;
}
.fhc-resizable {
position: relative;
}
.fhc-resizable .fhc-resize-bar
{
position:absolute; left:6px; right:6px; height:10px;
opacity:0; pointer-events:none; transition:opacity .12s ease;
cursor: ns-resize; z-index: 20;
}
.fhc-resizable:hover .fhc-resize-bar{ opacity:1; pointer-events:auto; }
.fhc-resize-bar--top{ top:-4px; }
.fhc-resize-bar--bottom{ bottom:-4px; }
.fhc-resize-bar::after{
content:""; display:block; margin:3px auto 0;
width:44px; height:4px; border-radius:999px;
background: rgba(0,0,0,.35);
}
.verband-selection {
border: 1px dashed #AAAAAA;
margin-bottom: 0.5rem;
max-height: 450px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.lecture-selection {
border: 1px dashed #AAAAAA;
margin-bottom: 0.5rem;
max-height: 300px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.course-picker {
border: 1px dashed #AAAAAA;
height: 400px;
max-height: 400px;
display: flex;
flex-direction: column;
}
.course-picker-row {
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid var(--bs-border-color);
background-color: var(--bs-tertiary-bg);
}
.room-selection {
border: 1px dashed #AAAAAA;
margin-bottom: 0.5rem;
padding: 0.5rem 0.0rem 0.5rem 0.5rem;
}
.btn-link.text-danger {
text-decoration: none;
font-weight: 600;
}
.btn-link.text-danger:hover {
color: #dc3545;
transform: scale(1.1);
}
.bg-lecturer-wish {
opacity: .15;
}
.bg-lecturer-block {
background: rgba(255, 0, 0, 0.15);
}
.wish-w--2 {
background-color: #FF2200;
}
.wish-w--1 {
background-color: #FF9922;
}
.wish-w-1 {
background-color: #CCFFCC;
}
.wish-w-2 {
background-color: #48FA66;
}
+37
View File
@@ -2,6 +2,10 @@
cursor: pointer;
font-size: var(--fhc-calendar-fontsize-event, .875rem);
}
.event--parked {
opacity: 0.45 !important;
}
.fhc-calendar-mode-day .fhc-calendar-base-grid-line,
.fhc-calendar-mode-week .fhc-calendar-base-grid-line {
padding: 0 var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
@@ -69,3 +73,36 @@
.fhc-calendar-base .event > *:hover {
filter: brightness(120%);
}
.fhc-resize-bar {
align-items: center;
justify-content: center;
height: 15px;
flex: 0 0 12px;
cursor: ns-resize;
position: relative;
visibility: hidden;
pointer-events: none;
}
.fhc-resize-bar--top {
margin-bottom: -16px;
}
.fhc-resize-bar--bottom {
margin-top: -16px;
}
.fhc-calendar-base-grid-line-event:hover > .fhc-resize-bar {
visibility: visible;
pointer-events: auto;
}
.fhc-calendar-base-grid-line-event.event:hover {
z-index: 50;
}
.fhc-resize-bar {
z-index: 2;
}
-10
View File
@@ -1,10 +0,0 @@
export default {
getCourses(searchfilter) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/getCourses',
params: { searchfilter }
};
}
};
-60
View File
@@ -1,60 +0,0 @@
export default {
getRoomplan(ort_kurzbz, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Kalender/getRoomplan',
params: { ort_kurzbz, start_date, end_date }
};
},
getStundenplan(start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Kalender/getStundenplan',
params: { start_date, end_date}
};
},
getStunden() {
return {
method: 'get',
url: '/api/frontend/v1/Kalender/Stunden',
params: {}
};
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return {
method: 'get',
url: `/api/frontend/v1/Kalender/Reservierungen/${ort_kurzbz}`,
params: { start_date, end_date}
};
},
getStundenplanReservierungen(start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Kalender/Reservierungen',
params: { start_date, end_date }
};
},
getLehreinheitStudiensemester(lehreinheit_id) {
return {
method: 'get',
url: `/api/frontend/v1/Kalender/getLehreinheitStudiensemester/${lehreinheit_id}`,
params: {}
};
},
updateKalenderEvent(kalender_id, ort_kurzbz, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/Kalender/updateKalenderEvent',
params: { kalender_id, ort_kurzbz, start_date, end_date}
};
},
addKalenderEvent(lehreinheit_id, ort_kurzbz, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/Kalender/addKalenderEvent',
params: { lehreinheit_id, ort_kurzbz, start_date, end_date}
};
},
};
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
get() {
return {
method: 'get',
url: 'api/frontend/v1/tempus/config/get'
};
},
getHeader() {
return {
method: 'get',
url: 'api/frontend/v1/tempus/config/getHeader'
};
},
set(params) {
return {
method: 'post',
url: 'api/frontend/v1/tempus/config/set',
params
};
}
};
@@ -0,0 +1,9 @@
export default {
search(query) {
return {
method: 'get',
url: 'api/frontend/v1/tempus/coursepicker/search',
params: { query }
};
}
};
+53
View File
@@ -0,0 +1,53 @@
export default {
getPlan(filter, start_date, end_date)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlan',
params: { ...filter, start_date, end_date }
};
},
getLektorZeitsperren(emp, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getZeitsperren',
params: { emp, start_date, end_date }
};
},
getLektorZeitwuensche(emp, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getZeitwuensche',
params: { emp, start_date, end_date }
};
},
getStunden() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getStunden',
};
},
updateKalenderEvent(kalender_id, updatedInfos) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/updateKalenderEvent',
params: { kalender_id, updatedInfos}
};
},
addKalenderEvent(lehreinheit_id, ort_kurzbz, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/addKalenderEvent',
params: { lehreinheit_id, ort_kurzbz, start_date, end_date}
};
},
getRaumvorschlag(start_date, end_date, lehreinheit_id) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getRaumvorschlag',
params: { start_date, end_date, lehreinheit_id}
};
},
};
-1
View File
@@ -16,7 +16,6 @@
*/
import FhcTempus from "../components/Tempus/Tempus.js";
import fhcapifactory from "./api/fhcapifactory.js";
import Phrasen from "../plugins/Phrasen.js";
+32 -12
View File
@@ -1,23 +1,31 @@
import BaseDraganddrop from './Base/DragAndDrop.js';
import BaseHeader from './Base/Header.js';
import BaseSlider from './Base/Slider.js';
import BsModal from '../Bootstrap/Modal.js';
import CalClick from '../../directives/Calendar/Click.js';
import DragClick from '../../directives/dragClick.js';
import Draggable from '../../directives/draggable.js';
import Drop from '../../directives/drop.js';
export default {
name: "CalendarBase",
components: {
BaseDraganddrop,
BaseHeader,
BaseSlider,
BsModal
},
directives: {
CalClick
CalClick,
DragClick,
Draggable,
Drop
},
provide() {
return {
events: Vue.computed(() => this.convertedEvents),
backgrounds: Vue.computed(() => this.convertedBackgrounds),
dropAllowed: Vue.computed(() => !!this.onDrop),
onDrop: Vue.computed(() => this.onDrop || null),
locale: Vue.computed(() => this.locale),
timezone: Vue.computed(() => this.timezone),
timeGrid: Vue.computed(() => this.timeGrid),
@@ -43,6 +51,17 @@ export default {
return () => true;
}),
resizableEvents: Vue.computed(() => {
if (!this.resizableEvents)
return () => false;
if (Array.isArray(this.resizableEvents))
return event => this.resizableEvents.includes(event.type);
if (this.resizableEvents instanceof Function)
return this.resizableEvents;
return () => true;
}),
hasDragoverFunc: Vue.computed(() => this.onDragover),
mode: Vue.computed(() => this.mode)
};
@@ -93,9 +112,14 @@ export default {
type: Boolean,
default: undefined
},
btnTableList: {
type: Boolean,
default: undefined
},
timeGrid: Array,
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
resizableEvents: [Boolean, Array, Function],
onDragover: Function,
onDrop: Function
},
@@ -240,9 +264,7 @@ export default {
break;
}
},
onDropItem(evt, start, end) {
this.$emit('drop', evt, start, end);
},
showEventModal(eventObj) {
this.modalEvent = eventObj;
this.$refs.modal.show();
@@ -263,11 +285,7 @@ export default {
},
template: /* html */`
<div class="fhc-calendar-base h-100">
<base-draganddrop
class="card h-100"
:events="convertedEvents"
:backgrounds="convertedBackgrounds"
@drop="onDropItem"
<div class="card h-100"
v-cal-click:container
@cal-click-default.capture="handleClickDefaults"
>
@@ -282,6 +300,7 @@ export default {
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:btn-table-list="!!modes['tableList'] && (btnTableList || (showBtns && btnTableList !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
>
<slot name="actions" />
@@ -293,11 +312,12 @@ export default {
@update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal"
@request-modal-close="hideEventModal"
@drop="$emit('drop', $event)"
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
</component>
</base-draganddrop>
</div>
<bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden">
<template #title>
<slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" />
@@ -1,165 +0,0 @@
import DragAndDrop from '../../../helpers/DragAndDrop.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
/**
* TODO(chris): this needs serious rework!
*/
export default {
name: "CalendarDragAndDrop",
directives: {
CalDnd
},
provide() {
return {
events: Vue.computed(() => this.correctedEvents),
backgrounds: Vue.computed(() => this.backgrounds),
dropAllowed: Vue.computed(() => this.dragging && this.dropAllowed)
};
},
inject: {
mode: "mode",
dropableEvents: "dropableEvents"
},
props: {
events: Array,
backgrounds: Array
},
emits: [
"drop"
],
data() {
return {
dragging: false,
allowed: false,
draggedInternalEvent: null,
draggedExternalEvent: null,
targetTimestamp: 0,
targetGridEnds: null,
dropAllowed: false,
shadowPreview: false // TODO(chris): IMPLEMENT! (use background instead of event as preview)
};
},
computed: {
correctedEvents() {
if (this.dragging) {
if (this.draggedInternalEvent) {
const index = this.events.findIndex(e => e.id == this.draggedInternalEvent.id);
if (this.previewEvent && !this.shadowPreview)
return this.events.toSpliced(index, 1, this.previewEvent);
else
return this.events.toSpliced(index, 1);
}
if (this.previewEvent && !this.shadowPreview)
return [...this.events, this.previewEvent];
}
return this.events;
},
correctedBackgrounds() {
if (this.dragging) {
if (this.shadowPreview) {
// TODO(chris): how to get the length
return [...this.backgrounds, {
start: new Date(this.targetTimestamp),
class: 'shadow-preview'
}];
}
}
return this.backgrounds;
},
previewEvent() {
if (!this.dragging || !this.dropAllowed)
return null;
if (!this.targetTimestamp)
return null;
const event = this.draggedInternalEvent || this.draggedExternalEvent;
if (!event)
return null;
// TODO(chris): calculate length correctly from orig
let length = event.end - event.start;
if (this.targetGridEnds)
length = this.targetGridEnds.find(end => end >= this.targetTimestamp + length) - this.targetTimestamp;
return {
orig: event.orig,
start: this.targetTimestamp,
end: this.targetTimestamp + length
};
}
},
methods: {
onDragstart(evt) {
const data = DragAndDrop.convertToTransferData(evt.detail.item.orig);
if (DragAndDrop.isValidDragObject(data)) {
DragAndDrop.setTransferData(evt.detail.originalEvent, data);
this.draggedInternalEvent = evt.detail.item;
}
},
onDragend() {
this.draggedInternalEvent = null;
this.dragging = false;
},
onDragenter(evt) {
this.dragging = true;
if (!this.draggedInternalEvent) {
const event = DragAndDrop.getValidTransferData(evt.detail.originalEvent);
if (event) {
this.draggedExternalEvent = {
id: event.id,
type: event.type,
start: event.isostart
? luxon.DateTime.fromISO(event.isostart).setZone(this.timezone)
: luxon.DateTime.local().setZone(this.timezone),
end: event.isoend
? luxon.DateTime.fromISO(event.isoend).setZone(this.timezone)
: luxon.DateTime.local().setZone(this.timezone),
orig: event
};
} else {
this.draggedExternalEvent = null;
}
this.dropAllowed = this.dropableEvents(event, this.mode);
} else {
this.dropAllowed = this.dropableEvents(this.draggedInternalEvent, this.mode);
}
},
onDragleave() {
this.dragging = false;
},
onDragchange(evt) {
this.targetTimestamp = evt.detail.timestamp;
this.targetGridEnds = evt.detail.ends || null;
},
onDrop(evt) {
if (!this.dragging || !this.dropAllowed)
return;
this.$emit('drop', evt, this.previewEvent.start, this.previewEvent.end);
this.dropAllowed = false;
this.dragging = false;
}
},
template: `
<div
class="fhc-calendar-base-draganddrop"
@calendar-dragstart="onDragstart"
@calendar-dragend="onDragend"
v-cal-dnd:dropcage
@calendar-dragenter="onDragenter"
@calendar-dragleave="onDragleave"
@calendar-dragchange="onDragchange"
@drop="onDrop"
>
<slot />
</div>
`
}
+180 -18
View File
@@ -1,7 +1,8 @@
import GridLine from './Grid/Line.js';
import GridLineEvent from './Grid/Line/Event.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
import drop from '../../../directives/drop.js';
import { useResizeHandler } from '../../../helpers/Tempus/ResizeHandler.js';
export default {
name: "CalendarGrid",
@@ -10,12 +11,17 @@ export default {
GridLineEvent
},
directives: {
CalDnd
drop
},
inject: {
originalEvents: "events",
originalBackgrounds: "backgrounds",
dropAllowed: "dropAllowed"
dropAllowed: "dropAllowed",
onDrop: "onDrop",
timeGrid: {
from: "timeGrid",
default: () => []
}
},
provide() {
return {
@@ -57,10 +63,10 @@ export default {
},
data() {
return {
dragging: false,
resizeObserver: null,
mutationObserver: null,
userScroll: true
userScroll: true,
isDragging: false
};
},
computed: {
@@ -262,7 +268,9 @@ export default {
mouseFrac = mouse / this.$refs.body.offsetHeight;
}
return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
let rawTimestamp = dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
let fiveMinutes = 5 * 60 * 1000;
return Math.round(rawTimestamp / fiveMinutes) * fiveMinutes;
},
/* SCROLLING */
@@ -308,7 +316,157 @@ export default {
} else {
this.$refs.scroller.scrollTo(0, 0);
}
}
},
calculateNettoDuration(start, end) {
const startDay = start.startOf('day');
const blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
let nettoDuration = luxon.Duration.fromMillis(0);
for (const block of blocks)
{
const blockStart = startDay.plus(block.start);
const blockEnd = startDay.plus(block.end);
const overlapStart = blockStart > start ? blockStart : start;
const overlapEnd = blockEnd < end ? blockEnd : end;
if (overlapStart < overlapEnd) {
nettoDuration = nettoDuration.plus(overlapEnd.diff(overlapStart));
}
}
return nettoDuration;
},
calculateDropEnd(dropStart, durationMs) {
const duration = luxon.Duration.fromMillis(durationMs);
const blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
let accumulated = luxon.Duration.fromMillis(0);
for (const block of blocks)
{
const blockStart = dropStart.startOf('day').plus(block.start);
const blockEnd = dropStart.startOf('day').plus(block.end);
if (blockEnd <= dropStart) continue;
const relevantStart = blockStart > dropStart ? blockStart : dropStart;
const relevantDuration = blockEnd.diff(relevantStart);
accumulated = accumulated.plus(relevantDuration);
if (accumulated >= duration)
{
const overflow = accumulated.minus(duration);
return blockEnd.minus(overflow);
}
}
const lastBlock = blocks[blocks.length - 1];
return dropStart.startOf('day').plus(lastBlock.end);
},
onDropSnap(evt, items, date, part) {
let obj = items?.[0];
if (!obj?.orig) return;
const dayStr = evt?.currentTarget?.dataset?.day;
const dropDay = dayStr ? luxon.DateTime.fromISO(dayStr) : date;
const dropStart = dropDay.plus(part.start || part);
let nettoDuration = this._getNettoDurationForDrop(obj);
let dropEnd = this.calculateDropEnd(dropStart, nettoDuration);
this.onDrop?.({
item: [obj],
start: dropStart.toISO(),
end: dropEnd.toISO()
});
},
_getNettoDurationForDrop(obj) {
if (!obj?.orig)
return luxon.Duration.fromObject({ minutes: 45 });
if (obj.orig.isostart && obj.orig.isoend)
{
let s = luxon.DateTime.fromISO(obj.orig.isostart);
let e = luxon.DateTime.fromISO(obj.orig.isoend);
if (s.isValid && e.isValid)
return this.calculateNettoDuration(s, e);
}
if (obj.type === 'lehreinheit')
{
let blockung = Number(obj.orig.blockung ?? 1);
if (!Number.isFinite(blockung) || blockung <= 0) blockung = 1;
let blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
let firstBlock = blocks[0];
let bStart = luxon.Duration.fromISO(firstBlock.start);
let bEnd = luxon.Duration.fromISO(firstBlock.end);
let blockMinutes = bEnd.minus(bStart).as('minutes');
if (!Number.isFinite(blockMinutes) || blockMinutes <= 0) blockMinutes = 45;
return luxon.Duration.fromObject({ minutes: blockung * blockMinutes });
}
return luxon.Duration.fromObject({ minutes: 45 });
},
onDropFree(evt, items, date)
{
let obj = items?.[0];
if (!obj?.orig)
return;
const timestamp = this.getTimestampFromMouse(evt, date);
const dropStart = luxon.DateTime.fromMillis(timestamp);
let nettoDuration = this._getNettoDurationForDrop(obj);
let dropEnd = this.calculateDropEnd(dropStart, nettoDuration);
this.onDrop?.({
item: items,
start: dropStart.toISO(),
end: dropEnd.toISO()
});
},
handleResizeStart({ edge, evt, el, event })
{
const gridEl = this.$refs.body;
if (!gridEl)
return;
this.resizeHandler.startResize(edge, evt, {
el,
gridEl,
event,
timeGrid: this.timeGrid,
onEnd: ({ event, newStart, newEnd }) => {
const orig = event?.orig;
if (!orig || !newStart || !newEnd)
return;
this.onDrop?.({
item: [{ type: 'kalender', id: orig.kalender_id, orig }],
start: newStart,
end: newEnd
});
}
});
},
},
setup()
{
const resizeHandler = useResizeHandler();
return { resizeHandler };
},
beforeUnmount() {
this.disableAutoScroll();
@@ -382,11 +540,11 @@ export default {
ref="body"
class="grid-body"
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid"
@dragenter="isDragging = true"
@dragleave.self="isDragging = false"
@dragend="isDragging = false"
@drop="isDragging = false"
:style="'grid-' + axisCol + ':2/-1;grid-' + axisRow + ':1/-1'"
v-cal-dnd:dropcage
@calendar-dragenter="dragging = true"
@calendar-dragleave="dragging = false"
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
@@ -401,9 +559,11 @@ export default {
>
<slot name="part-body" v-bind="{ index, part }" />
<div
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
v-if="snapToGrid"
style="position:absolute;inset:0"
:style="{ zIndex: isDragging ? 10 : 1 }"
:data-day="date.toFormat('yyyy-MM-dd')"
v-drop:move.lehreinheit-collection.kalender-collection="(evt, item) => onDropSnap(evt, item, date, part)"
></div>
</div>
<grid-line
@@ -413,6 +573,7 @@ export default {
:events="eventsNormal[index]"
:backgrounds="backgrounds[index]"
style="position:relative"
@resize-start="handleResizeStart"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
>
<template #event="slot">
@@ -420,9 +581,10 @@ export default {
</template>
<template #dropzone>
<div
v-if="!snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
v-if="!snapToGrid"
style="position:absolute;inset:0"
:style="{ zIndex: isDragging ? 10 : 1 }"
v-drop:move.lehreinheit-collection.kalender-collection="(evt, item) => onDropFree(evt, item, date)"
></div>
</template>
</grid-line>
@@ -432,4 +594,4 @@ export default {
</div>
</div>
`
}
}
@@ -74,6 +74,7 @@ export default {
:key="i"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
@resize-start="$emit('resize-start', $event)"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
@@ -1,15 +1,35 @@
import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
import draggable from '../../../../../directives/draggable.js';
import CalClick from '../../../../../directives/Calendar/Click.js';
export default {
name: "GridLineEvent",
directives: {
CalDnd,
draggable,
CalClick
},
emits: [
'resize-start'
],
data() {
return {
contextMenu: {
show: false,
x: 0,
y: 0
}
};
},
inject: {
draggableEvents: "draggableEvents",
mode: "mode"
resizableEvents: {
from: "resizableEvents",
default: () => () => false
},
mode: "mode",
contextMenuActions: {
from: "contextMenuActions",
default: () => ({})
}
},
props: {
event: {
@@ -27,6 +47,9 @@ export default {
draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
},
resizable() {
return !this.isHeaderOrFooter && this.resizableEvents(this.event.orig, this.mode);
},
classes() {
const classes = [];
if (this.isHeaderOrFooter) {
@@ -37,21 +60,93 @@ export default {
if (this.event.endsHere)
classes.push('event-end');
}
return classes
return classes;
},
dragKalenderCollection() {
const orig = this.event.orig;
return {
type: 'kalender',
id: orig?.kalender_id ?? null,
orig,
};
},
activeContextActions() {
if (this.isHeaderOrFooter) return [];
const type = this.event.orig?.type ?? 'lehreinheit';
return this.contextMenuActions[type] ?? this.contextMenuActions['default'] ?? [];
}
},
template: /* html */`
methods: {
onResizeStart(edge, evt) {
this.$emit('resize-start', {
edge,
evt,
el: this.$refs.eventEl,
event: this.event
});
},
onRightClick(evt) {
this.contextMenu.show = true;
this.contextMenu.x = evt.clientX;
this.contextMenu.y = evt.clientY;
},
onContextAction(action) {
this.contextMenu.show = false;
action(this.event.orig);
},
closeContextMenu() {
this.contextMenu.show = false;
}
},
template:`
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
style="z-index: 1"
style="z-index: 2"
:draggable="draggable"
v-cal-dnd:draggable="event"
ref="eventEl"
v-draggable:move.noimage="draggable ? dragKalenderCollection : {}"
v-cal-click:event="isHeaderOrFooter ? event : event.orig"
@contextmenu.prevent="onRightClick"
>
<div
v-if="resizable"
class="fhc-resize-bar fhc-resize-bar--top"
@pointerdown.prevent.stop="onResizeStart('start', $event)"
@click.stop
/>
<slot :event="isHeaderOrFooter ? event : event.orig">
{{ event.orig }}
</slot>
<div
v-if="resizable"
class="fhc-resize-bar fhc-resize-bar--bottom"
@pointerdown.prevent.stop="onResizeStart('end', $event)"
@click.stop
/>
<teleport to="body">
<div
v-if="contextMenu.show"
style="position:fixed; inset:0; z-index:9998"
@click="closeContextMenu"
@contextmenu.prevent="closeContextMenu"
/>
<ul
v-if="contextMenu.show"
class="dropdown-menu show"
:style="{ position: 'fixed', top: contextMenu.y + 'px', left: contextMenu.x + 'px', zIndex: 9999 }"
>
<li v-for="action in activeContextActions" :key="action.label">
<button class="dropdown-item" type="button" @click.stop="onContextAction(action.action)">
<i v-if="action.icon" :class="action.icon + ' me-2'"></i>
{{ action.label }}
</button>
</li>
</ul>
</teleport>
</div>
`
}
}
@@ -0,0 +1,32 @@
import draggable from '../../../../../directives/draggable.js';
export default {
name: 'EventCard',
directives: {
draggable,
},
props: {
event: { type: Object, required: true },
parked: Boolean
},
computed: {
dragKalenderCollection() {
return this.event
},
},
template: `
<div
class="fhc-calendar-base-grid-line-event event"
v-draggable:move.noimage="dragKalenderCollection"
style="border:1px"
>
<div class="title">
{{ event.orig.topic || event.orig.titel || event.orig.lehrfach }}
</div>
<div>
{{ event.orig.datum }} {{ event.orig.beginn }}{{ event.orig.ende }}
<span v-if="event.ort_kurzbz">· {{ event.orig.ort_kurzbz }}</span>
</div>
</div>
`
};
+12 -1
View File
@@ -24,7 +24,8 @@ export default {
btnMonth: Boolean,
btnWeek: Boolean,
btnDay: Boolean,
btnList: Boolean
btnList: Boolean,
btnTableList: Boolean
},
emits: [
"next",
@@ -89,6 +90,16 @@ export default {
>
<i class="fa fa-table-list"></i>
</button>
<button
v-if="btnTableList"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'tableList'}"
@click="clickMode($event, 'tableList')"
>
<i class="fa fa-table-list"></i>
</button>
</div>
</div>
</div>
@@ -39,6 +39,7 @@ export default {
case "list":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
case "week":
case "tableList":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day":
return this.convertedDate;
@@ -51,6 +52,7 @@ export default {
case "month":
return this.date.toLocaleString({ month: 'long', year: 'numeric' });
case "week":
case "tableList":
var year = this.date.localWeekYear;
var week = this.date.toFormat('nn');
return this.$p.t('calendar/year_kw', { year, week });
@@ -76,6 +78,7 @@ export default {
break;
case "list":
case "week":
case "tableList":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "day":
@@ -96,7 +99,7 @@ export default {
@update:model-value="update"
:format="() => title"
:month-picker="mode == 'month'"
:week-picker="mode == 'week'"
:week-picker="mode == 'week' || mode == 'tableList'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:text-input="mode == 'day'"
:week-start="weekStart"
@@ -1,7 +1,8 @@
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
import draggable from '../../../../directives/draggable.js';
import CalClick from '../../../../directives/Calendar/Click.js';
// TODO(chris): drag and drop
@@ -13,7 +14,7 @@ export default {
LabelDow
},
directives: {
CalDnd,
draggable,
CalClick
},
inject: {
@@ -86,7 +87,7 @@ export default {
v-else
class="event"
:draggable="draggable(event)"
v-cal-dnd:draggable="event"
v-draggable="event.orig"
v-cal-click:event="event.orig"
>
<slot :event="event.orig" mode="list" />
@@ -36,7 +36,9 @@ export default {
}
}
return events;
})
}),
draggableEvents: () => false,
resizableEvents: () => false
};
},
inject: {
+109
View File
@@ -0,0 +1,109 @@
import BaseSlider from '../Base/Slider.js';
import TableView from './Table/View.js';
export default {
name: "ModeTable",
components: {
BaseSlider,
TableView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ weeks: this.rangeOffset });
} else {
last = last.plus({ weeks: this.rangeOffset });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks;
if (this.rangeOffset) {
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(weeks) {
const newFocusDate = this.focusDate.plus({ weeks });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
},
viewAttrs(weeks) {
const day = this.focusDate.plus({ weeks });
return { ...this.$attrs, day };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'day':
// default: Set current-date
this.$emit('update:currentDate', evt.detail.value);
break;
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
},
template: `
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<table-view ref="view" v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
</table-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,147 @@
import {CoreFilterCmpt} from "../../../../components/filter/Filter.js";
import BsModal from '../../../Bootstrap/Modal.js';
import FormInput from "../../../Form/Input.js";
import ApiDetails from "../../../../api/lehrveranstaltung/details.js";
export default {
name: "TableView",
inject: {
events: "events",
timezone: "timezone"
},
components: {
CoreFilterCmpt,
BsModal,
FormInput
},
props: {
day: {
type: luxon.DateTime,
required: true
}
},
data()
{
return {
raumtyp_array: []
}
},
computed: {
start() {
return this.day.startOf('week', { useLocaleWeeks: true });
},
preparedEvents() {
const end = this.start.plus({ days: 7 });
return this.events
.filter(e => e.start < end && e.end > this.start)
.sort((a, b) => a.start.ts - b.start.ts)
.map(event => ({
...event.orig,
row_index: event.id,
}));
},
tabulatorOptions() {
return {
index: "row_index",
layout: 'fitDataStretch',
placeholder: "Keine Daten verfügbar",
persistenceID: "2026_03_09_table_view_v1",
data: this.preparedEvents,
columns: [
{
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
titleFormatterParams: {
rowRange: "active"
},
headerSort: false,
width: 40
},
{title: 'Datum', field: 'datum', headerFilter: "input", formatter: (cell) => {
let val = cell.getValue();
if (!val)
return '&nbsp;';
return luxon.DateTime.fromISO(val).toFormat('dd.MM.yyyy')
}
},
{title: 'Von', field: 'beginn', headerFilter: "input"},
{title: 'Bis', field: 'ende', headerFilter: "input"},
{title: 'Lehrfach', field: 'lehrfach', headerFilter: "input"},
{title: 'Bezeichnung', field: 'lehrfach_bez', headerFilter: "input"},
{title: 'Lehrform', field: 'lehrform', headerFilter: "input"},
{title: 'Raum', field: 'ort_kurzbz', headerFilter: "input"},
{
title: 'Lektor',
field: 'lektor',
headerFilter: "input",
mutator: (value) => {
if (!value)
return '';
return value.map(l => l.kurzbz).join(', ') ?? ''
}
},
{title: 'OE', field: 'organisationseinheit', headerFilter: "input"},
{title: 'Status', field: 'status_kurzbz', headerFilter: "input"},
]
}
}
},
methods:
{
openModal() {
this.$refs.raumModal.show();
}
},
watch: {
preparedEvents(newData) {
this.$refs.tableViewTable?.tabulator?.setData(newData);
}
},
mounted() {
this.$api.call(ApiDetails.getRaumtyp())
.then(result => {
this.raumtyp_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: /* html */`
<div class="fhc-calendar-mode-table-view h-100 overflow-auto">
<core-filter-cmpt
ref="tableViewTable"
:tabulator-options="tabulatorOptions"
:table-only="true"
:side-menu="false"
:download="true"
>
<template #actions>
<button class="btn btn-outline-secondary btn-sm">Verschieben</button>
<button @click="openModal" class="btn btn-outline-secondary btn-sm">Raum wechsel</button>
</template>
</core-filter-cmpt>
<bs-modal ref="raumModal" class="bootstrap-prompt" dialogClass="modal-lg">
<template #title>Raum verschiebung</template>
<form-input
:label="$p.t('lehre', 'raumtyp')"
type="select"
container-class="col-3"
name="raumtyp"
>
<option
v-for="raumtyp in raumtyp_array"
:value="raumtyp.raumtyp_kurzbz"
:key="raumtyp.raumtyp_kurzbz"
>
{{ raumtyp.raumtyp_kurzbz }} {{ raumtyp.beschreibung }}
</option>
</form-input>
<template #footer>
<button type="button" class="btn btn-primary">{{ $p.t('ui', 'speichern') }}</button>
</template>
</bs-modal>
</div>
`
}
+120 -26
View File
@@ -7,15 +7,25 @@ import { useEventLoader } from '../../composables/EventLoader.js';
import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeTable from './Mode/Table.js';
import ApiTempusConfig from '../../api/factory/tempus/config.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
export default {
name: "CalendarTempus",
components: {
FhcCalendar
},
inject: [
"renderers"
],
inject: {
renderers: {from: 'renderers'},
appConfig: {
from: 'appConfig',
default: {
visible_status: 'all'
}
}
},
props: {
timezone: {
type: String,
@@ -32,6 +42,18 @@ export default {
getPromiseFunc: {
type: Function,
required: true
},
parkedEvents: {
type: Object,
default: () => new Set()
},
visibleLecturers: {
type: Array,
default: null
},
extraBackgrounds: {
type: Array,
default: () => []
}
},
emits: [
@@ -40,11 +62,13 @@ export default {
"update:range",
"drop"
],
data() {
return {
modes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
month: Vue.markRaw(ModeMonth),
tableList: Vue.markRaw(ModeTable),
},
modeOptions: {
day: {
@@ -55,31 +79,78 @@ export default {
collapseEmptyDays: false
}
},
teachingunits: null
teachingunits: null,
visibleStatusArray: [],
visibleStatus: []
};
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
let past = [];
if (this.mode == 'Month')
return [
{
class: 'background-past',
end: now.startOf('day')
}
];
return [
{
{
past = [{
class: 'background-past',
end: now.startOf('day')
}];
}
else
{
past = [{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
}];
}
return [
...past,
...(this.extraBackgrounds || [])
];
}
},
visibleEvents()
{
let list = this.events;
if (Array.isArray(this.visibleLecturers))
{
const visibleLectures = new Set(this.visibleLecturers);
list = list.filter(event => {
if (!event.lektor?.length)
return true;
return event.lektor.some(lektor => visibleLectures.has(lektor.mitarbeiter_uid));
});
}
if (!this.visibleStatus.length || this.visibleStatus.includes('all'))
return list;
return list.filter(event => this.visibleStatus.includes(event.status_kurzbz));
},
},
methods: {
toggleStatus(status) {
if (status === 'all')
{
this.visibleStatus = ['all'];
return;
}
this.visibleStatus = this.visibleStatus.filter(visibleStatus => visibleStatus !== 'all');
let found = this.visibleStatus.indexOf(status);
if (found === -1)
this.visibleStatus.push(status);
else
this.visibleStatus.splice(found, 1);
if (this.visibleStatus.length < 1)
this.visibleStatus.push('all');
},
eventStyle(event) {
if (!event.farbe)
return undefined;
@@ -89,14 +160,18 @@ export default {
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
},
ondrop(e, start, end){
this.$emit('drop', e, start, end);
}
ondrop(payload){
this.$emit('drop', payload);
},
resetEventLoader() {
this.reset();
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
@@ -105,12 +180,14 @@ export default {
return {
rangeInterval,
events,
lv
lv,
reset
};
},
created() {
this.$api
.call(ApiLvPlan.getStunden())
.call(ApiKalender.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
@@ -118,6 +195,11 @@ export default {
end: el.ende
}));
});
this.$api.call(ApiTempusConfig.getHeader())
.then(res => {
this.visibleStatusArray = res.data.visible_status
this.visibleStatus = ['all']
});
},
template: /* html */`
<fhc-calendar
@@ -129,18 +211,20 @@ export default {
:mode="mode"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events || []"
:events="visibleEvents || []"
:backgrounds="backgrounds"
:time-grid="teachingunits"
show-btns
@drop="ondrop"
:draggable-events="true"
:resizable-events="true"
:on-drop="ondrop"
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
@update:range="updateRange"
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:class="['event-type-' + event.type + ' ' + mode + 'PageContainer', { 'event--parked': parkedEvents.has(String(event.kalender_id)) }]"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
@@ -162,7 +246,17 @@ export default {
</div>
</template>
<template #actions>
<slot />
<slot>
<button
v-for="(status, key) in visibleStatusArray"
:key="key"
class="btn btn-sm me-1"
:class="visibleStatus.includes(key) ? 'btn-secondary' : 'btn-outline-secondary'"
@click="toggleStatus(key)"
>
{{ status }}
</button>
</slot>
</template>
</fhc-calendar>`
}
@@ -31,7 +31,7 @@ export default {
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
].join(": "));
} else {console.log(this.event.lektor);
} else {
tooltipArray.push([
this.$p.t('lehre/lektor'),
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
@@ -64,7 +64,7 @@ export default {
<span>{{ start }}</span>
<span>{{ end }}</span>
</div>
<div class="event-text" v-tooltip="tooltipString">
<div class="event-text">
<span class="event-topic">{{ event.topic }}</span>
<span class="event-place">{{ event.ort_kurzbz }}</span>
<span
@@ -131,7 +131,7 @@ export default {
},
onSelectTreeNode(node) {
if (node.data.link)
this.$emit('selectVerband', {link: node.data.link, studiengang_kz: node.data.stg_kz, semester: node.data.semester, orgform_kurzbz: node.data.orgform_kurzbz});
this.$emit('selectVerband', {link: node.data.link, studiengang_kz: node.data.stg_kz, semester: node.data.semester, orgform_kurzbz: node.data.orgform_kurzbz, name: node.data.name});
},
mapNodesToNoSemReloadNodes(result, node) {
if (node.data.no_sem_reload)
+87 -54
View File
@@ -1,55 +1,71 @@
import ApiCoursePicker from '../../api/factory/coursepicker.js';
import FormInput from '../Form/Input.js';
import draggable from "../../directives/draggable.js";
import ApiCoursePicker from '../../api/factory/tempus/coursepicker.js';
export default {
components: {
FormInput
},
provide() {
return {
};
directives: {
draggable
},
data() {
return {
courses: null
searchparam: '',
courses: null,
abortController: null,
}
},
props: {
},
computed: {
},
methods: {
loadCourses: function(){
async loadCourses() {
Promise.allSettled([
this.$api.call(ApiCoursePicker.getCourses("test")),
]).then((result) => {
let promise_events = [];
result.forEach((promise_result) => {
if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") {
let data = promise_result.value.data;
if (data && data.forEach) {
const query = (this.searchparam ?? '').trim();
data.forEach((entry, i) => {
entry.showname = entry.studiengang_kurzbz+entry.semester+' - ' + entry.kurzbz + ' ' + entry.lektoren.toString();
entry.tag = '';
entry.mode = 'single';
});
if (!query)
{
this.courses = [];
return;
}
if (query.length < 3)
return;
if (this.abortController)
{
this.abortController.abort();
}
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.$api.call(ApiCoursePicker.search(query), { signal })
.then(result => {
this.courses = result.data.map(entry => ({
lehreinheit_id: entry.lehreinheit_id,
lektoren: entry.lektor,
studiengang: entry.studiengang,
semester: entry.semester,
stundenblockung: entry.stundenblockung,
wochenrythmus: entry.wochenrythmus,
showname: `${entry.lehrfach} ${entry.lehrform}`,
orig: {
type: 'lehreinheit',
lehreinheit_id: entry.lehreinheit_id,
blockung: entry.stundenblockung,
entry,
}
promise_events = promise_events.concat(data);
}
}));
})
this.courses = promise_events;
});
.catch(this.$fhcAlert.handleSystemError)
},
dragstart: function(evt, course) {
const transferdata = {
dragLehreinheitCollection(course) {
const orig = course.orig;
return {
type: 'lehreinheit',
id: course.lehreinheit_id,
mode: course.mode
id: orig.lehreinheit_id,
orig: orig,
stundenblockung: course.stundenblockung,
};
event.dataTransfer.setData('text', JSON.stringify(transferdata));
},
keydown: function(evt, course) {
@@ -64,26 +80,43 @@ export default {
course.mode = 'multi';
break
}
}
},
created() {
this.loadCourses();
//document.addEventListener('keydown', function () { console.log("a"); });
},
mounted() {
},
template: /*html*/`
<div ref="container" class="fhc-coursechooser">
<div id="coursechooser">
<span id="coursechooserheader">Course</span>
<input type="text" placeholder="Search"/>
<div v-for="course in courses" class="eckerl" draggable="true" @dragstart="dragstart(event, course)" tabindex="0" @keyup="keydown($event, course)">
{{course.showname}}
{{course.tag}}
</div>
<span id="coursechooserfooter">Drag & Drop on Calender</span>
template: `
<div class="course-picker">
<div class="p-2">
<form-input
:label="$p.t('ui', 'suche')"
type="text"
v-model="searchparam"
@input="loadCourses"
/>
</div>
</div>`
<div class="overflow-auto px-2 pb-2 flex-grow-1">
<div
v-for="course in courses"
:key="course.lehreinheit_id"
class="course-picker-row"
v-draggable:move.noimage="dragLehreinheitCollection(course)"
tabindex="0"
>
<div class="d-flex justify-content-between align-items-start">
<span class="fw-semibold small">{{ course.showname }}</span>
</div>
<div class="text-muted">
<span>{{ course.studiengang }} - {{ course.semester }}</span>
</div>
<div class="text-muted">
<span>{{ course.lektoren }}</span>
</div>
<div class="text-muted">
<span>WR: {{ course.wochenrythmus }} Bl: {{ course.stundenblockung }}</span>
</div>
</div>
</div>
<div class="mt-auto px-2 py-2 small text-muted border-top">
Drag & Drop on Calendar
</div>
</div>
`
}
@@ -0,0 +1,44 @@
import FormInput from '../Form/Input.js';
export default {
name: "LectureSelection",
props: {
lecturers: {
type: Array,
required: true
}
},
emits: ['remove'],
template: `
<div class="lecture-selection">
<div v-for="l in lecturers" :key="l.uid">
<div class="fw-semibold px-2 pt-2 d-flex align-items-center justify-content-between">
{{ l.label }}
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="$emit('remove', l.uid)"
title="Lektor entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="overflow-auto flex-grow-1 px-2 pb-2">
<div class="d-flex align-items-center gap-2" @click="l.showEvents = !l.showEvents" style="cursor:pointer">
<i :class="l.showEvents ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Plan</span>
</div>
<div class="d-flex align-items-center gap-2" @click="l.overlays.blocks = !l.overlays.blocks" style="cursor:pointer">
<i :class="l.overlays.blocks ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Zeitsperren</span>
</div>
<div class="d-flex align-items-center gap-2" @click="l.overlays.wishes = !l.overlays.wishes" style="cursor:pointer">
<i :class="l.overlays.wishes ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Zeitwünsche</span>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,78 @@
import EventCard from '../Calendar/Base/Grid/Line/EventCard.js';
import drop from '../../directives/drop.js';
export default {
name: "ParkingSlot",
components: {
EventCard
},
directives: {
drop
},
emits: ['update:parkedKeys'],
data() {
return {
parked: [],
parkedKeys: new Set()
};
},
methods: {
park(evt, items) {
const list = Array.isArray(items) ? items : [items];
const stored = JSON.parse(localStorage.getItem('tempus_parking') || '[]');
list.forEach(item => {
const key = `${item.id}`;
if (this.parkedKeys.has(key))
return;
this.parkedKeys.add(key);
stored.push({
type: item.type,
id: item.id,
orig: item.orig
});
this.parked.push(item);
});
localStorage.setItem('tempus_parking', JSON.stringify(stored));
this.$emit('update:parkedKeys', this.parkedKeys);
},
unpark(event) {
const key = `${event.id}`;
this.parkedKeys.delete(key);
this.parked = this.parked.filter(parkedEvent => parkedEvent.id !== event.id);
const stored = JSON.parse(localStorage.getItem('tempus_parking') || '[]').filter(parkedEvent => parkedEvent.id !== event.id);
localStorage.setItem('tempus_parking', JSON.stringify(stored));
this.$emit('update:parkedKeys', this.parkedKeys);
},
isParked(id) {
return this.parkedKeys.has(`${id}`);
}
},
mounted() {
const stored = JSON.parse(localStorage.getItem('tempus_parking') || '[]');
this.parked = stored;
this.parkedKeys = new Set(stored.map(store => `${store.id}`));
this.$emit('update:parkedKeys', this.parkedKeys);
},
template: `
<div class="overflow-auto" tabindex="-1">
<div
id="parkingslot"
class="parkingslot"
v-drop:move.kalender-collection="(evt, item) => park(evt, item)"
>
<i class="fa-solid fa-square-parking"></i>
<event-card
class="parkingevent"
v-for="parkedEvent in parked"
:key="parkedEvent.id"
:event="parkedEvent"
parked
/>
</div>
</div>
`
}
+14 -3
View File
@@ -111,7 +111,7 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
return mergePromiseArr(getPromiseFunc(start, end), result);
};
Vue.watchEffect(() => {
const reload = () => {
const range = Vue.toValue(rangeInterval);
if (!(range instanceof luxon.Interval))
return;
@@ -132,7 +132,18 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
}
})
});
})
};
return { events: allEvents, lv }
Vue.watchEffect(reload);
const reset = () => {
loading_id = 0;
events.value = [];
loadingEvents.value = [];
eventsLoaded.splice(0, eventsLoaded.length);
reload();
}
return { events: allEvents, lv, reset }
}
@@ -1,100 +0,0 @@
/**
* TODO(chris): This needs serious rework!!!
*/
export default {
mounted(el, binding, vnode) {
if (binding.arg == 'draggable') {
el.addEventListener('update-my-value', evt => {
evt.preventDefault();
binding.value = evt.detail.item;
});
el.addEventListener('dragstart', evt => {
el.dispatchEvent(new CustomEvent('calendar-dragstart', {
cancelable: true,
bubbles: true,
detail: {
item: binding.value,
x: evt.offsetX / el.offsetWidth,
y: evt.offsetY / el.offsetHeight,
originalEvent: evt
}
}));
});
el.addEventListener('dragend', evt => {
el.dispatchEvent(new CustomEvent('calendar-dragend', {
cancelable: true,
bubbles: true,
detail: {
item: binding.value,
originalEvent: evt
}
}));
});
} else if (binding.arg == 'dropcage') {
let hitbox = null;
el.addEventListener('dragover', evt => {
if (hitbox)
return;
hitbox = el.getBoundingClientRect();
return el.dispatchEvent(new CustomEvent('calendar-dragenter', {
detail: { originalEvent: evt }
}));
});
window.addEventListener('dragleave', evt => {
if (!hitbox)
return;
let pos;
if (typeof evt.clientX === 'undefined')
pos = {
x: evt.pageX + document.documentElement.scrollLeft,
y: evt.pageY + document.documentElement.scrollTop
};
else
pos = {
x: evt.clientX + document.body.scrollLeft + document.documentElement.scrollLeft,
y: evt.clientY + document.body.scrollTop + document.documentElement.scrollTop
};
if (pos.x > hitbox.left + hitbox.width - 1 || pos.x < hitbox.left || pos.y > hitbox.top + hitbox.height - 1 || pos.y < hitbox.top) {
hitbox = null;
return el.dispatchEvent(new CustomEvent('calendar-dragleave', {
detail: { originalEvent: evt }
}));
}
});
window.addEventListener('drop', evt => {
if (!hitbox)
return;
hitbox = null;
return el.dispatchEvent(new CustomEvent('calendar-dragleave', {
detail: { originalEvent: evt }
}));
});
} else if (binding.arg == 'dropzone') {
el.addEventListener(
binding.modifiers.once ? 'dragenter' : 'dragover',
evt => {
const timestamp = binding.value instanceof Function
? binding.value(evt)
: binding.value;
const detail = timestamp.timestamp ? timestamp : { timestamp };
el.dispatchEvent(new CustomEvent('calendar-dragchange', {
cancelable: true,
bubbles: true,
detail
}));
}
);
}
},
updated(el, binding, vnode, prevVnode) {
if (binding.arg == 'draggable') {
el.dispatchEvent(new CustomEvent('update-my-value', {
cancelable: true,
detail: {
item: binding.value
}
}));
}
}
}
+2 -1
View File
@@ -30,7 +30,8 @@ export default {
function onStart(evt) {
const value = el.dataset.fhcDraggableValue;
if (value) {
setTransferData(evt, JSON.parse(value), true);
let disableImage = binding.modifiers?.noimage === true;
setTransferData(evt, JSON.parse(value), !disableImage);
if (el.dataset.fhcEffectAllowed)
evt.dataTransfer.effectAllowed = el.dataset.fhcEffectAllowed;
+18 -9
View File
@@ -5,11 +5,13 @@
const TYPE_DEFINITION = {
lehreinheit: {
id: "lehreinheit_id",
dragIcon: "fa-solid fa-chalkboard-user",
extras: [
"stundenblockung"
]
},
kalender: {
id: "kalender_id",
},
vevent: {
id: "uid",
dragIcon: "fa-solid fa-calendar",
@@ -53,12 +55,16 @@ function isValidDragObject(value) {
if (!Object.prototype.hasOwnProperty.call(value, 'values'))
return false;
if (!VALID_TYPES.includes(value.type.substr(0, value.type.length-11)))
return false;
} else {
if (!Object.prototype.hasOwnProperty.call(value, 'id'))
return false;
if (!VALID_TYPES.includes(value.type))
return false;
@@ -182,10 +188,10 @@ function convertToValidDragObject(data, strict) {
const found = Object.entries(TYPE_DEFINITION).find(([ , typedef ]) => {
if (!Object.prototype.hasOwnProperty.call(data, typedef.id))
return false;
if (typedef.extras) {
/*if (typedef.extras) {
if (!typedef.extras.every(extra => Object.prototype.hasOwnProperty.call(data, extra)))
return false;
}
}*/
return true;
});
@@ -198,13 +204,16 @@ function convertToValidDragObject(data, strict) {
const newData = {};
newData.type = type;
newData.id = data[typedef.id];
if (typedef.extras)
typedef.extras.forEach(extra => newData[extra] = data[extra]);
/*if (typedef.extras)
typedef.extras.forEach(extra => newData[extra] = data[extra]);*/
return newData;
}
function setTransferData(event, validDragObject, setDragImage = false) {
if (setDragImage) {
const dragItems = Array.isArray(validDragObject) ? validDragObject : [ validDragObject ];
const dragElements = dragItems.map(item => {
@@ -246,9 +255,9 @@ function setTransferData(event, validDragObject, setDragImage = false) {
});
}
if (Array.isArray(validDragObject)) {
return validDragObject.forEach(data => setTransferData(event, data));
}
event.dataTransfer.setData('application/fhc-' + validDragObject.type, JSON.stringify(validDragObject));
}
@@ -267,16 +276,16 @@ function eventHasTypes(event, allowedTypes, strict) {
allowedTypes = allowedTypes.map(type => 'application/fhc-' + type);
const dataTypes = [...event.dataTransfer.types];
// NOTE(chris): if dragging across browsers the dataTransfer object is
// set to a default one without data. Since we do not support dragging
// across browsers (yet) we return false which will disallow dropping.
if (!dataTypes.length)
return false;
if (!strict)
return allowedTypes.some(type => [...event.dataTransfer.types].includes(type));
return [...event.dataTransfer.types].every(type => allowedTypes.includes(type));
}
+102
View File
@@ -0,0 +1,102 @@
export function useResizeGhost() {
let ghostEl = null;
let labelEl = null;
function create(gridEl, eventEl, edge)
{
const gridRect = gridEl.getBoundingClientRect();
const eventRect = eventEl.getBoundingClientRect();
const scrollTop = gridEl.scrollTop;
const topInGrid = (eventRect.top - gridRect.top) + scrollTop;
const leftInGrid = eventRect.left - gridRect.left;
ghostEl = document.createElement('div');
ghostEl.className = 'fhc-event-ghost';
ghostEl.style.cssText = `
position: absolute;
left: ${leftInGrid}px;
width: ${eventRect.width}px;
top: ${topInGrid}px;
height: ${eventRect.height}px;
z-index: 9999;
pointer-events: none;
box-sizing: border-box;
border-radius: 6px;
outline: 2px dashed currentColor;
opacity: 0.9;
`;
labelEl = document.createElement('div');
labelEl.className = 'fhc-resize-preview';
labelEl.style.cssText = `
position: absolute;
right: 6px;
padding: 2px 6px;
border-radius: 6px;
font-size: 12px;
background: rgba(0,0,0,0.75);
color: white;
white-space: nowrap;
pointer-events: none;
`;
if (edge === 'start')
{
labelEl.style.top = '6px';
labelEl.style.bottom = 'auto';
}
else
{
labelEl.style.top = 'auto';
labelEl.style.bottom = '6px';
}
ghostEl.appendChild(labelEl);
gridEl.style.position = 'relative';
gridEl.appendChild(ghostEl);
return {
startTop: topInGrid,
startHeight: eventRect.height
};
}
function updateLabel(text)
{
if (labelEl)
labelEl.textContent = text;
}
function updatePosition(top, height)
{
if (!ghostEl)
return;
if (top !== null)
ghostEl.style.top = `${top}px`;
if (height !== null)
ghostEl.style.height = `${height}px`;
}
function getPosition()
{
if (!ghostEl)
return { top: 0, height: 0 };
return {
top: parseFloat(ghostEl.style.top),
height: parseFloat(ghostEl.style.height)
};
}
function remove()
{
if (ghostEl?.parentNode)
ghostEl.parentNode.removeChild(ghostEl);
ghostEl = null;
labelEl = null;
}
return { create, updateLabel, updatePosition, getPosition, remove };
}
+217
View File
@@ -0,0 +1,217 @@
import { useResizeGhost } from './ResizeGhost.js';
const MIN_HEIGHT_PX = 20;
const MIN_DURATION_MIN = 5;
const SNAP_MINUTES = 5;
function snapToGrid(minutes, edge)
{
if (edge === 'start')
{
return minutes >= 0 ? Math.floor(minutes / SNAP_MINUTES) * SNAP_MINUTES : Math.ceil(minutes / SNAP_MINUTES) * SNAP_MINUTES;
}
return minutes >= 0 ? Math.ceil(minutes / SNAP_MINUTES) * SNAP_MINUTES : Math.floor(minutes / SNAP_MINUTES) * SNAP_MINUTES;
}
function getSnapTimes(timeGrid, dayISO, zoneName)
{
const parseTime = (time) =>
{
if (!time)
return null;
let dt = luxon.DateTime.fromFormat(`${dayISO} ${time}`, 'yyyy-MM-dd HH:mm:ss', { zone: zoneName });
if (!dt.isValid)
dt = luxon.DateTime.fromFormat(`${dayISO} ${time}`, 'yyyy-MM-dd HH:mm', { zone: zoneName });
return dt.isValid ? dt : null;
};
const startTimes = timeGrid.map(s => parseTime(s?.start)).filter(Boolean);
const endTimes = timeGrid.map(s => parseTime(s?.end)).filter(Boolean);
const sort = arr => arr.sort((a, b) => a.toMillis() - b.toMillis());
return {
start: sort(startTimes),
end: sort(endTimes),
};
}
function calculateNewTimes(activeResize, ghostPosition)
{
const { edge, event, timeGrid, startTop, startHeight } = activeResize;
const { start, end } = event;
const durationMinutes = end.diff(start, 'minutes').minutes;
if (!durationMinutes || durationMinutes <= 0)
return null;
const pxPerMinute = startHeight / durationMinutes;
let draggedPx = 0;
if (edge === 'end')
draggedPx = ghostPosition.height - startHeight;
if (edge === 'start')
draggedPx = ghostPosition.top - startTop;
const draggedMinutes = snapToGrid(draggedPx / pxPerMinute, edge);
let newStart = start;
let newEnd = end;
if (edge === 'start')
newStart = start.plus({ minutes: draggedMinutes });
if (edge === 'end')
newEnd = end.plus({ minutes: draggedMinutes });
if (Array.isArray(timeGrid) && timeGrid.length)
{
const snapTimes = getSnapTimes(timeGrid, start.toISODate(), start.zoneName);
if (edge === 'start')
{
const targets = snapTimes.start;
newStart = [...targets].reverse().find(t => t <= newStart) || targets[0];
}
else
{
const targets = snapTimes.end;
newEnd = targets.find(t => t >= newEnd) || targets[targets.length - 1];
}
}
return { newStart, newEnd };
}
export function useResizeHandler() {
const ghost = useResizeGhost();
let activeResize = null;
function getPointerYInGrid(evt)
{
const gridRect = activeResize.gridEl.getBoundingClientRect();
return (evt.clientY - gridRect.top) + activeResize.gridEl.scrollTop;
}
function updateGhostLabel()
{
const result = calculateNewTimes(activeResize, ghost.getPosition());
if (!result)
return;
ghost.updateLabel(`${result.newStart.toFormat('HH:mm')}${result.newEnd.toFormat('HH:mm')}`);
}
function onPointerMove(evt)
{
if (!activeResize || evt.pointerId !== activeResize.pointerId)
return;
evt.preventDefault();
const maxBottom = activeResize.gridEl.scrollHeight;
const pointerY = getPointerYInGrid(evt);
const draggedPx = pointerY - activeResize.dragStartY;
if (activeResize.edge === 'end')
{
let newHeight = Math.max(MIN_HEIGHT_PX, activeResize.startHeight + draggedPx);
if (activeResize.startTop + newHeight > maxBottom)
newHeight = maxBottom - activeResize.startTop;
ghost.updatePosition(null, newHeight);
}
else if (activeResize.edge === 'start')
{
let newTop = activeResize.startTop + draggedPx;
let newHeight = activeResize.startHeight - draggedPx;
if (newTop < 0)
{
newHeight -= (0 - newTop);
newTop = 0;
}
if (newHeight < MIN_HEIGHT_PX)
{
newTop = (activeResize.startTop + activeResize.startHeight) - MIN_HEIGHT_PX;
newHeight = MIN_HEIGHT_PX;
}
ghost.updatePosition(newTop, newHeight);
}
updateGhostLabel();
}
function onPointerUp(evt)
{
if (!activeResize || evt.pointerId !== activeResize.pointerId)
return;
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
if (activeResize.eventEl)
activeResize.eventEl.style.opacity = activeResize.originalOpacity ?? '';
const result = calculateNewTimes(activeResize, ghost.getPosition());
ghost.remove();
if (result)
{
if ((activeResize.event.start.toISO() !== result.newStart.toISO()) || (activeResize.event.end.toISO() !== result.newEnd.toISO()))
{
activeResize.onEnd({
event: activeResize.event,
newStart: result.newStart.toISO(),
newEnd: result.newEnd.toISO()
});
}
}
activeResize = null;
}
function startResize(edge, evt, { el, gridEl, event, timeGrid, onEnd })
{
const { startTop, startHeight } = ghost.create(gridEl, el, edge);
activeResize = {
edge,
pointerId: evt.pointerId,
eventEl: el,
gridEl,
event,
timeGrid,
onEnd,
dragStartY: (evt.clientY - gridEl.getBoundingClientRect().top) + gridEl.scrollTop,
startTop,
startHeight,
originalOpacity: el.style.opacity,
};
el.style.opacity = '0.35';
ghost.updateLabel(`${event.start.toFormat('HH:mm')}${event.end.toFormat('HH:mm')}`);
evt.currentTarget.setPointerCapture(evt.pointerId);
window.addEventListener('pointermove', onPointerMove, { passive: false });
window.addEventListener('pointerup', onPointerUp, { passive: false });
}
function cleanup()
{
if (!activeResize)
return;
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
ghost.remove();
activeResize = null;
}
return { startResize, cleanup };
}