mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-19 16:02:15 +00:00
Merge branch 'merge_C4_25999_61235_61730_calendar' into merge_C4_25999_61235_61730
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
// Define configuration parameters
|
||||
$config['timezone'] = 'Europe/Vienna';
|
||||
@@ -15,6 +15,9 @@ class LvPlan extends Auth_Controller
|
||||
parent::__construct([
|
||||
'index' => ['basis/cis:r']
|
||||
]);
|
||||
|
||||
// Load Config
|
||||
$this->load->config('calendar');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
@@ -28,6 +31,7 @@ class LvPlan extends Auth_Controller
|
||||
|
||||
$viewData = array(
|
||||
'uid'=>getAuthUID(),
|
||||
'timezone' => $this->config->item('timezone')
|
||||
);
|
||||
|
||||
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'LvPlan']);
|
||||
|
||||
@@ -17,6 +17,9 @@ class Cis4 extends Auth_Controller
|
||||
'index' => 'basis/cis:r'
|
||||
)
|
||||
);
|
||||
|
||||
// Load Config
|
||||
$this->load->config('calendar');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
@@ -27,15 +30,16 @@ class Cis4 extends Auth_Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->load->model('person/Person_model','PersonModel');
|
||||
$this->load->model('person/Person_model', 'PersonModel');
|
||||
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
|
||||
|
||||
$viewData = array(
|
||||
'uid' => getAuthUID(),
|
||||
'name' => $personData->vorname,
|
||||
'person_id' => $personData->person_id
|
||||
'person_id' => $personData->person_id,
|
||||
'timezone' => $this->config->item('timezone')
|
||||
);
|
||||
|
||||
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'FhcDashboard']);
|
||||
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'FhcDashboard']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +87,14 @@ class Cms extends Auth_Controller
|
||||
$this->load->view('CisRouterView/CisRouterView.php', ['viewData'=>$viewData, 'route' => 'News']);
|
||||
}
|
||||
|
||||
public function getRoomInformation($ort_kurzbz){
|
||||
public function getRoomInformation($ort_kurzbz)
|
||||
{
|
||||
// Load Config
|
||||
$this->load->config('calendar');
|
||||
|
||||
$viewData = array(
|
||||
'ort_kurzbz' => $ort_kurzbz
|
||||
'ort_kurzbz' => $ort_kurzbz,
|
||||
'timezone' => $this->config->item('timezone')
|
||||
);
|
||||
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'CmsRoom']);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class LvPlan extends FHCAPI_Controller
|
||||
parent::__construct([
|
||||
'getRoomplan' => self::PERM_LOGGED,
|
||||
'Stunden' => self::PERM_LOGGED,
|
||||
'Reservierungen' => self::PERM_LOGGED,
|
||||
'getReservierungen' => self::PERM_LOGGED,
|
||||
'LvPlanEvents' => self::PERM_LOGGED,
|
||||
'getLehreinheitStudiensemester' => self::PERM_LOGGED,
|
||||
'studiensemesterDateInterval' => self::PERM_LOGGED,
|
||||
@@ -177,55 +177,69 @@ class LvPlan extends FHCAPI_Controller
|
||||
$this->terminateWithSuccess($stunden);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetches room events from a certain date
|
||||
* @access public
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* fetches room events from a certain date
|
||||
* @access public
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getRoomplan()
|
||||
{
|
||||
// form validation
|
||||
$this->load->library('form_validation');
|
||||
|
||||
$this->form_validation->set_rules('ort_kurzbz', "Ort", "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())
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
// storing the post parameter in local variables
|
||||
$ort_kurzbz = $this->input->post('ort_kurzbz', true);
|
||||
$start_date = $this->input->post('start_date', true);
|
||||
$end_date = $this->input->post('end_date', true);
|
||||
|
||||
// get data
|
||||
$this->load->library('StundenplanLib');
|
||||
// form validation
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('ort_kurzbz',"Ort","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
|
||||
$ort_kurzbz = $this->input->get('ort_kurzbz', TRUE);
|
||||
$start_date = $this->input->get('start_date', TRUE);
|
||||
$end_date = $this->input->get('end_date', TRUE);
|
||||
|
||||
$roomplan_data = $this->LvPlan->lvPlanGruppierung($this->LvPlan->getRoomQuery($ort_kurzbz, $start_date, $end_date));
|
||||
|
||||
$roomplan_data = $this->getDataOrTerminateWithError($roomplan_data);
|
||||
$this->stundenplanlib->expand_object_information($roomplan_data);
|
||||
$roomplan_data = $this->stundenplanlib->getRoomplan($ort_kurzbz, $start_date, $end_date);
|
||||
|
||||
$roomplan_data = $this->getDataOrTerminateWithError($roomplan_data);
|
||||
|
||||
$this->terminateWithSuccess($roomplan_data);
|
||||
|
||||
}
|
||||
|
||||
// gets the reservierungen of a room if the ort_kurzbz parameter is supplied otherwise gets the reservierungen of the lvplan of a student
|
||||
public function Reservierungen($ort_kurzbz = null)
|
||||
/**
|
||||
* gets the reservierungen of a room if the ort_kurzbz parameter is
|
||||
* supplied otherwise gets the reservierungen of the lvplan of a student
|
||||
* @access public
|
||||
*
|
||||
* @param string $ort_kurzbz
|
||||
* @return void
|
||||
*/
|
||||
public function getReservierungen($ort_kurzbz = null)
|
||||
{
|
||||
$this->load->library('StundenplanLib');
|
||||
//form validation
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
|
||||
$this->form_validation->set_rules('start_date', "StartDate", "required");
|
||||
$this->form_validation->set_rules('end_date', "EndDate", "required");
|
||||
if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
if (!$this->form_validation->run())
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$this->load->model('ressource/Mitarbeiter_model','MitarbeiterModel');
|
||||
// storing the post parameter in local variables
|
||||
$start_date = $this->input->post('start_date', true);
|
||||
$end_date = $this->input->post('end_date', true);
|
||||
|
||||
// storing the get parameter in local variables
|
||||
$start_date = $this->input->get('start_date', TRUE);
|
||||
$end_date = $this->input->get('end_date', TRUE);
|
||||
// get data
|
||||
$this->load->library('StundenplanLib');
|
||||
|
||||
$result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz);
|
||||
|
||||
$result = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$result = $this->stundenplanlib->getReservierungen($start_date,$end_date,$ort_kurzbz);
|
||||
$result = $this->getDataOrTerminateWithError($result);
|
||||
$this->terminateWithSuccess($result);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
use \DateTime as DateTime;
|
||||
use \DateTimeZone as DateTimeZone;
|
||||
use \DateInterval as DateInterval;
|
||||
use \DatePeriod as DatePeriod;
|
||||
|
||||
class StundenplanLib{
|
||||
|
||||
private $_ci; // Code igniter instance
|
||||
@@ -14,9 +19,11 @@ class StundenplanLib{
|
||||
*
|
||||
*/
|
||||
public function getStundenplan($start_date, $end_date, $lv_id = null){
|
||||
|
||||
$this->_ci =& get_instance();
|
||||
|
||||
// Load Config
|
||||
$this->_ci->load->config('calendar');
|
||||
|
||||
$this->_ci->load->model('ressource/Mitarbeiter_model','MitarbeiterModel');
|
||||
$this->_ci->load->model('organisation/Studiensemester_model','StudiensemesterModel');
|
||||
$this->_ci->load->model('education/Studentlehrverband_model', 'StudentlehrverbandModel');
|
||||
@@ -123,26 +130,69 @@ class StundenplanLib{
|
||||
|
||||
}
|
||||
|
||||
public function getReservierungen($start_date, $end_date, $ort_kurzbz){
|
||||
/**
|
||||
* Get stundenplan for a room
|
||||
*
|
||||
* @param string $ort_kurzbz
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @return stdClass
|
||||
*/
|
||||
public function getRoomplan($ort_kurzbz, $start_date, $end_date)
|
||||
{
|
||||
$this->_ci =& get_instance();
|
||||
|
||||
// Load Config
|
||||
$this->_ci->load->config('calendar');
|
||||
// Load Models
|
||||
$this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel');
|
||||
|
||||
$query = $this->_ci->StundenplanModel->getRoomQuery($ort_kurzbz, $start_date, $end_date);
|
||||
$roomplan_data = $this->_ci->StundenplanModel->stundenplanGruppierung($query);
|
||||
|
||||
if (isError($roomplan_data))
|
||||
return $roomplan_data;
|
||||
|
||||
$this->expand_object_information($roomplan_data->retval);
|
||||
|
||||
return $roomplan_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reservations (for a room or all)
|
||||
*
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @param string $ort_kurzbz
|
||||
* @return stdClass
|
||||
*/
|
||||
public function getReservierungen($start_date, $end_date, $ort_kurzbz = '')
|
||||
{
|
||||
$this->_ci =& get_instance();
|
||||
|
||||
// Load Config
|
||||
$this->_ci->load->config('calendar');
|
||||
// Load Models
|
||||
$this->_ci->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
|
||||
|
||||
$is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter(getAuthUID()));
|
||||
if($is_mitarbeiter)
|
||||
{
|
||||
|
||||
if ($is_mitarbeiter) {
|
||||
$reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date, $ort_kurzbz);
|
||||
} else {
|
||||
// querying the reservierungen
|
||||
$reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz);
|
||||
}
|
||||
if(isError($reservierungen))
|
||||
{
|
||||
return error(getData($reservierungen));
|
||||
}
|
||||
$reservierungen = getData($reservierungen) ?? [];
|
||||
$function_error = $this->expand_object_information($reservierungen);
|
||||
if(!is_null($function_error)){
|
||||
|
||||
if (isError($reservierungen))
|
||||
return $reservierungen;
|
||||
|
||||
$function_error = $this->expand_object_information($reservierungen->retval);
|
||||
|
||||
if (!is_null($function_error))
|
||||
return $function_error;
|
||||
}
|
||||
return success($reservierungen);
|
||||
|
||||
return $reservierungen;
|
||||
}
|
||||
|
||||
public function getLektorenFromLehrveranstaltung($lehrveranstaltung_id, $semester, $studiengang_kz, $studiensemester_kurzbz){
|
||||
@@ -181,8 +231,15 @@ class StundenplanLib{
|
||||
|
||||
public function expand_object_information($data){
|
||||
$this->_ci =& get_instance();
|
||||
|
||||
foreach ($data as $item)
|
||||
{
|
||||
$tz = new DateTimeZone($this->_ci->config->item('timezone'));
|
||||
$isostart = new DateTime($item->datum . ' ' . $item->beginn, $tz);
|
||||
$item->isostart = $isostart->format(DateTime::ATOM);
|
||||
|
||||
$isoend = new DateTime($item->datum . ' ' . $item->ende, $tz);
|
||||
$item->isoend = $isoend->format(DateTime::ATOM);
|
||||
|
||||
$lektor_obj_array = array();
|
||||
$gruppe_obj_array = array();
|
||||
@@ -254,9 +311,17 @@ class StundenplanLib{
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchFerienTageEvents($start_date, $end_date, $studiengang_kz){
|
||||
public function fetchFerienTageEvents($start_date, $end_date, $studiengang_kz)
|
||||
{
|
||||
$this->_ci =& get_instance();
|
||||
$this->_ci->load->model('organisation/Ferien_model','FerienModel');
|
||||
|
||||
// Load Config
|
||||
$this->_ci->load->config('calendar');
|
||||
|
||||
$this->_ci->load->model('organisation/Ferien_model', 'FerienModel');
|
||||
|
||||
$tz = new DateTimeZone($this->_ci->config->item('timezone'));
|
||||
|
||||
$ferienEvents = $this->_ci->FerienModel->execReadOnlyQuery("
|
||||
SELECT *
|
||||
FROM lehre.tbl_ferien
|
||||
@@ -277,35 +342,41 @@ class StundenplanLib{
|
||||
|
||||
$interval = new DateInterval('P1D');
|
||||
$period = new DatePeriod($event_start, $interval, $event_end);
|
||||
$event->dates = array_map(function($date){
|
||||
return $date->format('Y-m-d');
|
||||
}, iterator_to_array($period));
|
||||
$event->dates = iterator_to_array($period);
|
||||
return $event;
|
||||
}, $ferienEvents);
|
||||
|
||||
$start_date = new DateTime($start_date);
|
||||
$start_date->setTime(0, 0, 0);
|
||||
$end_date = new DateTime($end_date);
|
||||
$end_date->setTime(23, 59, 59);
|
||||
|
||||
$ferienEventsFlattened=[];
|
||||
foreach($ferienEvents as $ferien_event){
|
||||
foreach($ferien_event->dates as $date){
|
||||
if ($date < $start_date || $date > $end_date)
|
||||
continue;
|
||||
$event = new stdClass();
|
||||
$event->bezeichnung = $ferien_event->bezeichnung;
|
||||
$event->datum = $date;
|
||||
$event->datum = $date->format('Y-m-d');
|
||||
$event->type = 'ferien';
|
||||
$ferienEventsFlattened[] = $event;
|
||||
}
|
||||
};
|
||||
|
||||
$today=new DateTime();
|
||||
$ferienEventsFlattened = array_map(function($event) use($today){
|
||||
$ferienEventsFlattened = array_map(function($event) use($today, $tz){
|
||||
$ferien_event = (object) array(
|
||||
'type' => 'ferien',
|
||||
'beginn' => $today->format('H:i:s'),
|
||||
'ende' => $today->format('H:i:s'),
|
||||
'isostart' => (new DateTime($event->datum . ' 00:00:00', $tz))->format('c'),
|
||||
'isoend' => (new DateTime($event->datum . ' 23:59:59', $tz))->format('c'),
|
||||
'allDayEvent' => true,
|
||||
'datum' => $event->datum,
|
||||
'topic' => $event->bezeichnung,
|
||||
'titel' => $event->bezeichnung,
|
||||
'farbe' => '00689E'
|
||||
|
||||
);
|
||||
return $ferien_event;
|
||||
}, $ferienEventsFlattened);
|
||||
@@ -635,7 +706,7 @@ class StundenplanLib{
|
||||
{
|
||||
return $sem->studiensemester_kurzbz;
|
||||
},
|
||||
getData($semester_range)
|
||||
getData($semester_range) ?: []
|
||||
);
|
||||
|
||||
// if no studiensemester is found for the given timespan, get the nearest studiensemester
|
||||
|
||||
@@ -15,7 +15,7 @@ $includesArray = array(
|
||||
'public/css/components/searchbar/searchbar.css',
|
||||
'public/css/Fhc.css',
|
||||
'public/css/components/dashboard.css',
|
||||
'public/css/components/calendar.css',
|
||||
//'public/css/components/calendar.css', <= imported in dashboard.css
|
||||
'public/css/components/Sprachen.css',
|
||||
'public/css/components/MyLv.css',
|
||||
'public/css/components/FilterComponent.css',
|
||||
@@ -29,7 +29,8 @@ $includesArray = array(
|
||||
'vendor/npm-asset/primevue/accordiontab/accordiontab.min.js',
|
||||
'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js',
|
||||
'vendor/npm-asset/primevue/textarea/textarea.min.js',
|
||||
'vendor/npm-asset/primevue/checkbox/checkbox.min.js'
|
||||
'vendor/npm-asset/primevue/checkbox/checkbox.min.js',
|
||||
'vendor/moment/luxonjs/luxon.min.js'
|
||||
),
|
||||
'customJSModules' => array(
|
||||
'public/js/apps/Dashboard/Fhc.js'
|
||||
@@ -39,7 +40,7 @@ $includesArray = array(
|
||||
|
||||
$this->load->view('templates/CISVUE-Header', $includesArray);
|
||||
?>
|
||||
<div id="fhccontent" route=<?php echo $route ?>>
|
||||
<div id="fhccontent" class="h-100" route=<?php echo $route ?>>
|
||||
<router-view
|
||||
:view-data='<?php echo json_encode($viewData) ?>'
|
||||
></router-view>
|
||||
|
||||
@@ -414,6 +414,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "moment/luxonjs",
|
||||
"version": "3.6.1",
|
||||
"dist": {
|
||||
"url": "https://unpkg.com/luxon@3.6.1/build/global/luxon.min.js",
|
||||
"type": "file"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://asset-packagist.org"
|
||||
@@ -462,6 +473,7 @@
|
||||
|
||||
"michelf/php-markdown": "1.5.0",
|
||||
"ml/json-ld": "1.*",
|
||||
"moment/luxonjs": "3.6.1",
|
||||
"moment/momentjs": "2.24.0",
|
||||
"mottie/tablesorter": "2.*",
|
||||
|
||||
|
||||
Generated
+10
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1b8a408a43c62cb966fa89af338564a7",
|
||||
"content-hash": "1de37a74ba51a66057eb2712b21340c8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "afarkas/html5shiv",
|
||||
@@ -1296,6 +1296,15 @@
|
||||
},
|
||||
"time": "2022-09-29T08:45:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "moment/luxonjs",
|
||||
"version": "3.6.1",
|
||||
"dist": {
|
||||
"type": "file",
|
||||
"url": "https://unpkg.com/luxon@3.6.1/build/global/luxon.min.js"
|
||||
},
|
||||
"type": "library"
|
||||
},
|
||||
{
|
||||
"name": "moment/momentjs",
|
||||
"version": "2.24.0",
|
||||
|
||||
@@ -143,27 +143,27 @@
|
||||
|
||||
.dayPageContainer .feiertagEventContent #ferienEventTitle {}
|
||||
|
||||
/* Minimized Container Styles */
|
||||
/* List Page Container Styles */
|
||||
|
||||
/* Ferientage Events for Minimized */
|
||||
/* Ferientage Events for List Page */
|
||||
|
||||
.minimizedContainer .feiertagEventContent{
|
||||
.listPageContainer .feiertagEventContent{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--fhc-light);
|
||||
}
|
||||
|
||||
.minimizedContainer .feiertagEventContent #ferienEventIcon{
|
||||
.listPageContainer .feiertagEventContent #ferienEventIcon{
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.minimizedContainer .feiertagEventContent #ferienEventTitle{
|
||||
.listPageContainer .feiertagEventContent #ferienEventTitle{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Lehreinheits Events in Minimized */
|
||||
.dayPageContainer .lehreinheitEventContent {
|
||||
/* Lehreinheits Events in List Page */
|
||||
.listPageContainer .lehreinheitEventContent {
|
||||
display: flex;
|
||||
padding-left: 4rem;
|
||||
justify-content: space-evenly;
|
||||
@@ -171,15 +171,15 @@
|
||||
|
||||
}
|
||||
|
||||
.dayPageContainer .lehreinheitEventContent #lehreinheitEventHeader {
|
||||
.listPageContainer .lehreinheitEventContent #lehreinheitEventHeader {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.dayPageContainer .lehreinheitEventContent #lehreinheitEventText {
|
||||
.listPageContainer .lehreinheitEventContent #lehreinheitEventText {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.dayPageContainer .lehreinheitEventContent #lehreinheitEventText * {
|
||||
.listPageContainer .lehreinheitEventContent #lehreinheitEventText * {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,441 +1,38 @@
|
||||
:root{
|
||||
--fhc-calendar-pane-height: calc(100vh - 220px);
|
||||
|
||||
--fhc-calendar-primary: var(--fhc-primary, #006095);
|
||||
@import './calendar/frame.css';
|
||||
@import './calendar/events.css';
|
||||
@import './calendar/backgrounds.css';
|
||||
|
||||
/* Themable Variables */
|
||||
:root {
|
||||
--fhc-calendar-gap: var(--fhc-border, #dee2e6);
|
||||
--fhc-calendar-bg: var(--fhc-background, #fff);
|
||||
--fhc-calendar-border: var(--fhc-border, #dee2e6);
|
||||
--fhc-calendar-border-highlight: var(--fhc-border-highlight, #495057);
|
||||
--fhc-calendar-text: var(--fhc-text, #212529);
|
||||
--fhc-calendar-text-light: var(--fhc-text, #212529);
|
||||
--fhc-calendar-background: var(--fhc-background, #fff);
|
||||
--fhc-calendar-dark: var(--fhc-dark, #212529);
|
||||
--fhc-calendar-hour-indicator-bg: var(--fhc-background, #fff);
|
||||
--fhc-calendar-week-page-header-background: var(--fhc-background, #fff);
|
||||
--fhc-calendar-week-page-header-color: var(--fhc-text, #212529);
|
||||
--fhc-calendar-week-page-header-border: var(--fhc-border, #dee2e6);
|
||||
--fhc-calendar-week-page-header-hover-background: var(--fhc-background-highlight, #d5dae0);
|
||||
--fhc-calendar-all-day-event-background: var(--fhc-background, #fff);
|
||||
--fhc-calendar-past: var(--fhc-beige-10, rgba(245, 233, 215, 0.5));
|
||||
--fhc-calendar-box-shadow: var(--fhc-box-shadow, #dee2e6);
|
||||
}
|
||||
|
||||
:root.dark{
|
||||
--fhc-calendar-past: var(--fhc-beige-20, rgba(172, 153, 125, 0.5));
|
||||
}
|
||||
--fhc-calendar-bg-event: var(--fhc-white-80, #ccc);
|
||||
--fhc-calendar-border-event: var(--bs-secondary, #6c757d);
|
||||
--fhc-calendar-radius-event: var(--bs-border-radius, .375rem);
|
||||
--fhc-calendar-fontsize-event: .875em;
|
||||
|
||||
.fhc-calendar-pane {
|
||||
height: var(--fhc-calendar-pane-height);
|
||||
}
|
||||
--fhc-calendar-bg-markings-past: var(--fhc-beige-10, rgba(245, 233, 215, 0.5));
|
||||
--fhc-calendar-border-markings-past: var(--fhc-calendar-border);
|
||||
--fhc-calendar-bg-markings-past-label: var(--fhc-calendar-bg);
|
||||
--fhc-calendar-border-markings-past-label: var(--fhc-calendar-border);
|
||||
|
||||
.fhc-calendar-hour-indicator{
|
||||
border-top: 1px solid var(--fhc-calendar-border);
|
||||
}
|
||||
--fhc-calendar-gap-header: transparent;
|
||||
--fhc-calendar-gap-allday: transparent;
|
||||
--fhc-calendar-gap-body: transparent;
|
||||
--fhc-calendar-bg-header: var(--fhc-calendar-bg);
|
||||
--fhc-calendar-bg-allday: var(--fhc-calendar-bg);
|
||||
--fhc-calendar-bg-body: var(--fhc-calendar-bg);
|
||||
|
||||
.fhc-calendar-hour-indicator span{
|
||||
background-color: var(--fhc-calendar-hour-indicator-bg) !important;
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page-header{
|
||||
color: var(--fhc-calendar-text);
|
||||
background-color: var(--fhc-calendar-week-page-header-background);
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page-header > div {
|
||||
border-right: 1px solid var(--fhc-calendar-border);
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page-header > div:hover{
|
||||
background-color: var(--fhc-calendar-week-page-header-hover-background);
|
||||
}
|
||||
|
||||
.fhc-calendar-months .col-4,
|
||||
.fhc-calendar-years .col-4 {
|
||||
padding: 0.1875em 0;
|
||||
}
|
||||
.show-weeks .fhc-calendar-months .col-4,
|
||||
.show-weeks .fhc-calendar-years .col-4 {
|
||||
padding: 0.1875em 0.25em;
|
||||
}
|
||||
.fhc-calendar-months .col-4 button,
|
||||
.fhc-calendar-years .col-4 button {
|
||||
aspect-ratio: 28/18;
|
||||
}
|
||||
|
||||
.fhc-calendar-month-page {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: 1.5em repeat(6, 1fr);
|
||||
}
|
||||
.fhc-calendar-month-page.show-weeks {
|
||||
grid-template-columns: 1.5em repeat(7, 1fr);
|
||||
grid-template-rows: 1.5em repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.fhc-calendar-month-page-weekday,
|
||||
.fhc-calendar-month-page-day {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.fhc-calendar-week .carousel-item {
|
||||
/*padding: 0.75em;*/
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page {
|
||||
/*aspect-ratio: 7/6;*/
|
||||
min-height: 0;
|
||||
}
|
||||
.fhc-calendar-week-page > div {
|
||||
/*transform: translate(-0.75em, -0.75em);*/
|
||||
/*width: calc(100% + 1.5em);*/
|
||||
/*height: calc(100% + 1.5em);*/
|
||||
max-height: 100%;
|
||||
}
|
||||
.fhc-calendar-week-page > div > div {
|
||||
padding-left: 3em;
|
||||
}
|
||||
.fhc-calendar-week-page .events {
|
||||
display: grid;
|
||||
grid-template-columns: 3em repeat(7, 1fr);
|
||||
margin-left: -3em;
|
||||
/*min-height: 266.6666666667%;*/
|
||||
}
|
||||
.fhc-calendar-week-page .events .day {
|
||||
gap: 1px;
|
||||
}
|
||||
.fhc-calendar-week-page .events .hours, .fhc-calendar-week-page .events .day {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .all-day-event-border{
|
||||
box-shadow: 1px 1px 0 var(--fhc-calendar-box-shadow);
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .all-day-event {
|
||||
max-height: 75px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .all-day-event-container{
|
||||
position: sticky;
|
||||
top: 44px;
|
||||
display:grid;
|
||||
grid-template-columns:repeat(7,1fr);
|
||||
width:100%;
|
||||
z-index:3;
|
||||
background-color:var(--fhc-calendar-all-day-event-background);
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .all-day-event-container::before {
|
||||
position: absolute;
|
||||
content:'';
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -3em;
|
||||
right: 100%;
|
||||
background-color: var(--fhc-calendar-all-day-event-background);
|
||||
box-shadow: 1px 1px 0 var(--fhc-calendar-box-shadow);
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .all-day-event-border {
|
||||
box-shadow: 0 0 0 1px var(--fhc-calendar-box-shadow);
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .all-day-event {
|
||||
grid-column:2;
|
||||
max-height: 75px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .all-day-event-container {
|
||||
position: sticky;
|
||||
top:0;
|
||||
display: grid;
|
||||
grid-template-columns: 3em 1fr;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
background-color:var(--fhc-calendar-all-day-event-background);
|
||||
box-shadow: 1px 1px 0 var(--fhc-calendar-box-shadow);
|
||||
}
|
||||
|
||||
/* grid hour lines of the Stundenplan use box-shadow instead of border because box-shadow renders consistently on different viewports*/
|
||||
.box-shadow-border{
|
||||
box-shadow: 0 0 0 1px var(--fhc-calendar-border) !important;
|
||||
}
|
||||
|
||||
.fhc-calendar-no-events-overlay{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fhc-calendar-no-events-overlay::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 3rem;
|
||||
margin:auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(120deg, var(--fhc-calendar-background), var(--fhc-calendar-dark) );
|
||||
opacity: .7;
|
||||
}
|
||||
.fhc-calendar-day-page {
|
||||
/*aspect-ratio: 7/6;*/
|
||||
min-height: 0;
|
||||
}
|
||||
.fhc-calendar-day-page > div {
|
||||
height: calc(100% + 1.5em);
|
||||
}
|
||||
.fhc-calendar-day-page .flex-column > .flex-grow-1 {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .events {
|
||||
display: grid;
|
||||
grid-template-columns: 3em 1fr;
|
||||
}
|
||||
.fhc-calendar-day-page .events .day {
|
||||
gap: 1px;
|
||||
}
|
||||
.fhc-calendar-day-page .events .hours, .fhc-calendar-day-page .events .day {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.fhc-calendar-month-page-day{
|
||||
color: var(--fhc-calendar-text) !important;
|
||||
}
|
||||
|
||||
.fhc-calendar-month-page-day.fhc-calendar-past:not(.fhc-calendar-month-page-day-focusday) {
|
||||
color: var(--fhc-calendar-text) !important;
|
||||
}
|
||||
|
||||
.fhc-calendar-lg .fhc-calendar-month-page-day,
|
||||
.fhc-calendar-md .fhc-calendar-month-page-day {
|
||||
border: solid 1px var(--fhc-calendar-border);
|
||||
}
|
||||
|
||||
.fhc-calendar-lg .fhc-calendar-month-page-weekday,
|
||||
.fhc-calendar-md .fhc-calendar-month-page-weekday{
|
||||
border: solid 1px var(--fhc-secondary);
|
||||
}
|
||||
|
||||
|
||||
.fhc-calendar-lg .fhc-calendar-month-page-day,
|
||||
.fhc-calendar-md .fhc-calendar-month-page-day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.fhc-calendar-lg .fhc-calendar-month-page-day.active,
|
||||
.fhc-calendar-md .fhc-calendar-month-page-day.active {
|
||||
border-color: var(--fhc-calendar-border-highlight);
|
||||
}
|
||||
/*.fhc-calendar-lg .fhc-calendar-month-page-day .events,*/
|
||||
/*.fhc-calendar-md .fhc-calendar-month-page-day .events {*/
|
||||
/* display: block;*/
|
||||
/* overflow: auto;*/
|
||||
/* font-size: 0.7em;*/
|
||||
/*}*/
|
||||
/*.fhc-calendar-lg .fhc-calendar-month-page-day .events span,*/
|
||||
/*.fhc-calendar-md .fhc-calendar-month-page-day .events span {*/
|
||||
/* display: block;*/
|
||||
/* margin: 0.2em;*/
|
||||
/* padding: 0.1em 0.4em;*/
|
||||
/* border-radius: 0.1em;*/
|
||||
/*}*/
|
||||
.fhc-calendar-lg .fhc-calendar-years .col-4,
|
||||
.fhc-calendar-md .fhc-calendar-years .col-4 {
|
||||
padding: 0.09375em 0;
|
||||
}
|
||||
.show-weeks .fhc-calendar-lg .fhc-calendar-years .col-4,
|
||||
.show-weeks .fhc-calendar-md .fhc-calendar-years .col-4 {
|
||||
padding: 0.09375em 0.25em;
|
||||
}
|
||||
.fhc-calendar-lg .fhc-calendar-years .col-4 button,
|
||||
.fhc-calendar-md .fhc-calendar-years .col-4 button {
|
||||
aspect-ratio: 28/9;
|
||||
}
|
||||
|
||||
.fhc-calendar-xs {
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.fhc-calendar-sm .fhc-calendar-month-page {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.fhc-calendar-sm .fhc-calendar-month-page-weekday,
|
||||
.fhc-calendar-xs .fhc-calendar-month-page-weekday {
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.fhc-calendar-month-page-weekday:hover {
|
||||
background-color:var(--fhc-calendar-primary);
|
||||
color:var(--fhc-calendar-text-light);
|
||||
--fhc-calendar-bg-month-today: var(--fhc-primary);
|
||||
--fhc-calendar-color-month-today: var(--fhc-white-10);
|
||||
|
||||
--fhc-calendar-bg-overlay1: var(--fhc-calendar-bg);
|
||||
--fhc-calendar-bg-overlay2: var(--fhc-dark, #212529);
|
||||
|
||||
--fhc-calendar-radius-day-event: var(--fhc-calendar-radius-event);
|
||||
--fhc-calendar-radius-week-event: var(--fhc-calendar-radius-event);
|
||||
--fhc-calendar-radius-month-event: 0;
|
||||
--fhc-calendar-radius-list-event: var(--fhc-calendar-radius-event);
|
||||
}
|
||||
|
||||
.fhc-calendar-sm .fhc-calendar-month-page-day,
|
||||
.fhc-calendar-xs .fhc-calendar-month-page-day {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.fhc-calendar-past {
|
||||
background-color: var(--fhc-calendar-past);
|
||||
border-color: var(--fhc-calendar-border);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.fhc-calendar-month-page-day-focusday {
|
||||
border-color: var(--fhc-calendar-border-highlight) !important;
|
||||
background-color: var(--fhc-calendar-background);
|
||||
position: relative;
|
||||
animation: dash-animation 2.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash-animation {
|
||||
0% {
|
||||
border-style: dashed;
|
||||
}
|
||||
50% {
|
||||
border-style: solid;
|
||||
}
|
||||
100% {
|
||||
border-style: dashed;
|
||||
}
|
||||
}
|
||||
|
||||
.fhc-highlight-week {
|
||||
/*border-color: black !important;*/
|
||||
}
|
||||
|
||||
.fhc-highlight-day {
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.fhc-highlight-day::before {
|
||||
content:'';
|
||||
position:absolute;
|
||||
top:0;
|
||||
bottom:0;
|
||||
left:0;
|
||||
right:0;
|
||||
box-shadow: inset 0 0 0 2px var(--fhc-calendar-border) !important;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.fhc-calendar-sm .fhc-calendar-month-page-day.active .no,
|
||||
.fhc-calendar-xs .fhc-calendar-month-page-day.active .no {
|
||||
background-color: var(--fhc-calendar-primary);
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.fhc-calendar-sm .fhc-calendar-month-page-day:not(.active):hover .no,
|
||||
.fhc-calendar-xs .fhc-calendar-month-page-day:not(.active):hover .no {
|
||||
background-color: var(--fhc-calendar-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
/*.fhc-calendar-sm .fhc-calendar-month-page-day .no,*/
|
||||
/*.fhc-calendar-xs .fhc-calendar-month-page-day .no {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: center;*/
|
||||
/* width: 80%;*/
|
||||
/* height: 80%;*/
|
||||
/* margin: 10%;*/
|
||||
/*}*/
|
||||
/*.fhc-calendar-sm .fhc-calendar-month-page-day .events,*/
|
||||
/*.fhc-calendar-xs .fhc-calendar-month-page-day .events {*/
|
||||
/* position: absolute;*/
|
||||
/* bottom: 0;*/
|
||||
/* left: 10%;*/
|
||||
/* width: 80%;*/
|
||||
/* height: 10%;*/
|
||||
/* overflow: hidden;*/
|
||||
/* display: flex;*/
|
||||
/*}*/
|
||||
.fhc-calendar-sm .fhc-calendar-month-page-day .events span,
|
||||
.fhc-calendar-xs .fhc-calendar-month-page-day .events span {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectedEvent {
|
||||
background-color: var(--fhc-calendar-primary) !important;
|
||||
color: var(--fhc-calendar-text-light);
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .curTimeIndicator{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
padding-left: 7rem;
|
||||
margin-top: -1px;
|
||||
z-index: 2;
|
||||
border-color: var(--fhc-calendar-border)!important;
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .curTimeIndicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
padding-left: 1rem;
|
||||
margin-top: -1px;
|
||||
z-index: 2;
|
||||
border-color: var(--fhc-calendar-border) !important;
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .past::before{
|
||||
content:"";
|
||||
background-color: var(--fhc-calendar-past);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
opacity: 0.5;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fhc-calendar-week-page .overlay {
|
||||
background-color: var(--fhc-calendar-past);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .overlay {
|
||||
background-color: var(--fhc-calendar-past);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dayPageContainer,
|
||||
.weekPageContainer {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
#calendarHeaderTitle{
|
||||
color: var(--fhc-text);
|
||||
}
|
||||
|
||||
.fhc-calendar-day-page .hours,
|
||||
.fhc-calendar-day-page .curTimeIndicator,
|
||||
.fhc-calendar-day-page .fhc-calendar-hour-indicator,
|
||||
.fhc-calendar-week-page .hours,
|
||||
.fhc-calendar-week-page .curTimeIndicator,
|
||||
.fhc-calendar-week-page .fhc-calendar-hour-indicator,
|
||||
.fhc-calendar-day-page .date,
|
||||
.fhc-calendar-week-page .date {
|
||||
color: var(--fhc-text-secondary) !important;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.background-past {
|
||||
background-color: var(--fhc-calendar-bg-markings-past);
|
||||
border-bottom: solid 1px var(--fhc-calendar-border-markings-past);
|
||||
}
|
||||
.background-past > span {
|
||||
display: none;
|
||||
}
|
||||
.background-past.bg-end > span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 1rem;
|
||||
padding: 0 .5rem;
|
||||
background-color: var(--fhc-calendar-bg-markings-past-label);
|
||||
border: solid 1px var(--fhc-calendar-border-markings-past-label);
|
||||
font-size: .875em;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
.event {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fhc-calendar-mode-week .all-day-events .event .weekPageContainer {
|
||||
padding: .25rem;
|
||||
}
|
||||
.fhc-calendar-mode-day .event,
|
||||
.fhc-calendar-mode-week .event {
|
||||
margin: 0 .5rem;
|
||||
font-size: var(--fhc-calendar-fontsize-event, .875rem);
|
||||
}
|
||||
.fhc-calendar-mode-day .all-day-events .event,
|
||||
.fhc-calendar-mode-week .all-day-events .event {
|
||||
margin: .25rem;
|
||||
}
|
||||
/* Border */
|
||||
.fhc-calendar-mode-day .grid-main .event > *,
|
||||
.fhc-calendar-mode-week .grid-main .event > * {
|
||||
border: solid 1px var(--fhc-calendar-border-event);
|
||||
}
|
||||
.fhc-calendar-mode-day .event {
|
||||
--event-radius: var(--fhc-calendar-radius-day-event, 0);
|
||||
}
|
||||
.fhc-calendar-mode-week .event {
|
||||
--event-radius: var(--fhc-calendar-radius-week-event, 0);
|
||||
}
|
||||
.fhc-calendar-mode-month .event {
|
||||
--event-radius: var(--fhc-calendar-radius-month-event, 0);
|
||||
}
|
||||
.fhc-calendar-mode-list .event {
|
||||
--event-radius: var(--fhc-calendar-radius-list-event, 0);
|
||||
}
|
||||
.fhc-calendar-mode-day .all-day-events .event > *,
|
||||
.fhc-calendar-mode-week .all-day-events .event > *,
|
||||
.fhc-calendar-mode-day .event.event-begin > *,
|
||||
.fhc-calendar-mode-week .event.event-begin > * {
|
||||
border-top-left-radius: var(--event-radius);
|
||||
border-top-right-radius: var(--event-radius);
|
||||
}
|
||||
.fhc-calendar-mode-day .all-day-events .event > *,
|
||||
.fhc-calendar-mode-week .all-day-events .event > *,
|
||||
.fhc-calendar-mode-day .event.event-end > *,
|
||||
.fhc-calendar-mode-week .event.event-end > * {
|
||||
border-bottom-left-radius: var(--event-radius);
|
||||
border-bottom-right-radius: var(--event-radius);
|
||||
}
|
||||
.fhc-calendar-mode-month .event.event-begin > * {
|
||||
border-top-left-radius: var(--event-radius);
|
||||
border-bottom-left-radius: var(--event-radius);
|
||||
}
|
||||
.fhc-calendar-mode-month .event.event-end > * {
|
||||
border-top-right-radius: var(--event-radius);
|
||||
border-bottom-right-radius: var(--event-radius);
|
||||
}
|
||||
.fhc-calendar-mode-list .event > * {
|
||||
border-radius: var(--event-radius);
|
||||
}
|
||||
/* Colors */
|
||||
.fhc-calendar-base .event {
|
||||
--event-default-bg: var(--fhc-calendar-bg-event, #ccc);
|
||||
}
|
||||
.fhc-calendar-base .event .event-type-reservierung {
|
||||
--event-default-bg: var(--fhc-calendar-bg-reservierung, #fff);
|
||||
}
|
||||
.fhc-calendar-base .event > * {
|
||||
background-color: var(--event-bg, var(--event-default-bg));
|
||||
}
|
||||
.fhc-calendar-base .event > *:hover {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/* Labels */
|
||||
/* ====== */
|
||||
.fhc-calendar-base-label-dow .short,
|
||||
.fhc-calendar-base-label-dow .narrow,
|
||||
.fhc-calendar-base-label-day .full,
|
||||
.fhc-calendar-base-label-day .short,
|
||||
.fhc-calendar-base-label-day .narrow,
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-label-day .long,
|
||||
.collapsed-header .fhc-calendar-base-label-day .long,
|
||||
.collapsed-header .fhc-calendar-base-label-dow .long {
|
||||
display: none;
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-label-day .narrow,
|
||||
.collapsed-header .fhc-calendar-base-label-day .short,
|
||||
.collapsed-header .fhc-calendar-base-label-dow .short {
|
||||
display: block;
|
||||
}
|
||||
.fhc-calendar-mode-day .fhc-calendar-base-label-day,
|
||||
.fhc-calendar-mode-week .fhc-calendar-base-label-day {
|
||||
font-size: .875em;
|
||||
}
|
||||
.fhc-calendar-mode-day .main-header,
|
||||
.fhc-calendar-mode-week .main-header {
|
||||
text-align: center;
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-label-week {
|
||||
text-align: right;
|
||||
}
|
||||
.fhc-calendar-base-label-week {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
.fhc-calendar-base-label-time,
|
||||
.fhc-calendar-base-label-day,
|
||||
.fhc-calendar-base-label-week {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
.fhc-calendar-mode-day .fhc-calendar-base-label-dow,
|
||||
.fhc-calendar-mode-week .fhc-calendar-base-label-dow,
|
||||
.fhc-calendar-base-label-day,
|
||||
.fhc-calendar-base-label-week {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fhc-calendar-base-label-week span + span:before {
|
||||
content: "/";
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event.event-header {
|
||||
height: 100%;
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-label-day {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event.event-header {
|
||||
--event-bg: transparent;
|
||||
font-weight: bold;
|
||||
text-indent: 0;
|
||||
font-size: var(--fhc-calendar-fontsize-month-day, 1rem);
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event.event-header .disabled {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
|
||||
/* Base Grid */
|
||||
/* ========= */
|
||||
/* Grid */
|
||||
.fhc-calendar-base-grid {
|
||||
--fhc-calendar-axis-collapsible: .2fr;
|
||||
background-color: var(--fhc-calendar-gap);
|
||||
grid-gap: 2px;
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-header {
|
||||
background-color: var(--fhc-calendar-gap-header);
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-header:before,
|
||||
.fhc-calendar-base-grid .grid-allday:before {
|
||||
content: "";
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-header:before,
|
||||
.fhc-calendar-base-grid .grid-header .main-header {
|
||||
background-color: var(--fhc-calendar-bg-header);
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-allday {
|
||||
background-color: var(--fhc-calendar-gap-allday);
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-allday:before,
|
||||
.fhc-calendar-base-grid .grid-allday .all-day-events {
|
||||
background-color: var(--fhc-calendar-bg-allday);
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-main {
|
||||
grid-gap: 1px;
|
||||
background-color: var(--fhc-calendar-gap-body);
|
||||
}
|
||||
.fhc-calendar-base-grid .grid-main .part-header,
|
||||
.fhc-calendar-base-grid .grid-main .part-body {
|
||||
background-color: var(--fhc-calendar-bg-body);
|
||||
}
|
||||
.fhc-calendar-base-grid-line {
|
||||
grid-gap: 1px;
|
||||
}
|
||||
.fhc-calendar-base-label-time {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
}
|
||||
/* Header */
|
||||
.fhc-calendar-base-header > .row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Modes */
|
||||
/* ===== */
|
||||
/* Day */
|
||||
.fhc-calendar-mode-day .event-details {
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
border-left: solid 1px var(--fhc-calendar-gap);
|
||||
}
|
||||
.fhc-calendar-mode-day .today {
|
||||
background-color: var(--fhc-calendar-bg-day-today);
|
||||
color: var(--fhc-calendar-color-day-today);
|
||||
}
|
||||
/* Week */
|
||||
.fhc-calendar-mode-week .today {
|
||||
background-color: var(--fhc-calendar-bg-week-today);
|
||||
color: var(--fhc-calendar-color-week-today);
|
||||
}
|
||||
/* Month */
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line {
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
grid-template-rows: 2em;
|
||||
grid-auto-rows: calc(var(--fhc-calendar-fontsize-event, .875rem) + 2 * var(--fhc-calendar-margin-event-icon, .5rem));
|
||||
font-size: var(--fhc-calendar-fontsize-event, .875rem);
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event {
|
||||
text-indent: .5em;
|
||||
height: calc(var(--fhc-calendar-fontsize-event, .875rem) + 2 * var(--fhc-calendar-margin-event-icon, .5rem));
|
||||
overflow: hidden;
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event .placeholder-glow {
|
||||
text-indent: 0;
|
||||
}
|
||||
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event * {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fhc-calendar-mode-month .event > .today {
|
||||
background-color: var(--fhc-calendar-bg-month-today);
|
||||
color: var(--fhc-calendar-color-month-today);
|
||||
}
|
||||
/* List */
|
||||
.fhc-calendar-mode-list .today {
|
||||
background-color: var(--fhc-calendar-bg-list-today);
|
||||
color: var(--fhc-calendar-color-list-today);
|
||||
}
|
||||
|
||||
/* Overlays */
|
||||
/* ======== */
|
||||
/* No Events */
|
||||
.fhc-calendar-no-events-overlay {
|
||||
background: linear-gradient(120deg, var(--fhc-calendar-bg-overlay1), var(--fhc-calendar-bg-overlay2));
|
||||
opacity: .7;
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.fhc-calendar-no-events-overlay:before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 100px;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import './calendar.css';
|
||||
@import './dashboard/news.css';
|
||||
@import './dashboard/LvPlan.css';
|
||||
|
||||
:root{
|
||||
--fhc-dashboard-danger: var(--fhc-danger, #842029);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.dashboard-widget-lvplan .fhc-calendar-base .card {
|
||||
--bs-card-cap-padding-x: 0;
|
||||
--bs-card-cap-padding-y: 0;
|
||||
--bs-card-border-radius: 0;
|
||||
--bs-card-border-width: 0;
|
||||
--dp-input-padding: .375rem .75rem;
|
||||
--dp-input-icon-padding: calc(1rem + 24px);
|
||||
}
|
||||
.dashboard-widget-lvplan .fhc-calendar-base .card-header {
|
||||
border-bottom-width: var(--bs-border-width);
|
||||
}
|
||||
.dashboard-widget-lvplan .fhc-calendar-base .card-header > .row > .col-auto,
|
||||
.dashboard-widget-lvplan .fhc-calendar-base .card-header .btn-group {
|
||||
width: 100%;
|
||||
}
|
||||
.dashboard-widget-lvplan .fhc-calendar-base .card-header input {
|
||||
--dp-font-size: .7em;
|
||||
}
|
||||
.dashboard-widget-lvplan .fhc-calendar-base .card-header .btn {
|
||||
--bs-btn-font-size: .7em;
|
||||
}
|
||||
.dashboard-widget-lvplan .fhc-calendar-base-label-day {
|
||||
cursor: initial;
|
||||
}
|
||||
.dashboard-widget-lvplan .fhc-calendar-mode-list-view {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
export default {
|
||||
getRoomInfo(ort_kurzbz, start_date, end_date) {
|
||||
return {
|
||||
method: 'get',
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/LvPlan/getRoomplan',
|
||||
params: { ort_kurzbz, start_date, end_date }
|
||||
};
|
||||
@@ -39,15 +39,15 @@ export default {
|
||||
},
|
||||
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/LvPlan/Reservierungen/${ort_kurzbz}`,
|
||||
method: 'post',
|
||||
url: `/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
|
||||
params: { start_date, end_date }
|
||||
};
|
||||
},
|
||||
getLvPlanReservierungen(start_date, end_date) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/LvPlan/Reservierungen',
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/LvPlan/getReservierungen',
|
||||
params: { start_date, end_date }
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
export default {
|
||||
getRoomInfo(ort_kurzbz, start_date, end_date) {
|
||||
return this.$fhcApi.get(
|
||||
return this.$fhcApi.post(
|
||||
'/api/frontend/v1/LvPlan/getRoomplan',
|
||||
{ ort_kurzbz, start_date, end_date}
|
||||
);
|
||||
@@ -13,14 +13,14 @@ export default {
|
||||
);
|
||||
},
|
||||
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
|
||||
return this.$fhcApi.get(
|
||||
`/api/frontend/v1/LvPlan/Reservierungen/${ort_kurzbz}`,
|
||||
return this.$fhcApi.post(
|
||||
`/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
|
||||
{ start_date, end_date}
|
||||
);
|
||||
},
|
||||
getLvPlanReservierungen(start_date, end_date) {
|
||||
return this.$fhcApi.get(
|
||||
'/api/frontend/v1/LvPlan/Reservierungen',
|
||||
return this.$fhcApi.post(
|
||||
'/api/frontend/v1/LvPlan/getReservierungen',
|
||||
{ start_date, end_date }
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
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';
|
||||
|
||||
export default {
|
||||
name: "CalendarBase",
|
||||
components: {
|
||||
BaseDraganddrop,
|
||||
BaseHeader,
|
||||
BaseSlider,
|
||||
BsModal
|
||||
},
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
locale: Vue.computed(() => this.locale),
|
||||
timezone: Vue.computed(() => this.timezone),
|
||||
timeGrid: Vue.computed(() => this.timeGrid),
|
||||
draggableEvents: Vue.computed(() => {
|
||||
if (!this.draggableEvents)
|
||||
return () => false;
|
||||
|
||||
if (Array.isArray(this.draggableEvents))
|
||||
return event => this.draggableEvents.includes(event.type);
|
||||
if (this.draggableEvents instanceof Function)
|
||||
return this.draggableEvents;
|
||||
|
||||
return () => true;
|
||||
}),
|
||||
dropableEvents: Vue.computed(() => {
|
||||
if (!this.onDrop)
|
||||
return () => false;
|
||||
|
||||
if (Array.isArray(this.dropableEvents))
|
||||
return item => this.dropableEvents.includes(item.type);
|
||||
if (this.dropableEvents instanceof Function)
|
||||
return this.dropableEvents;
|
||||
|
||||
return () => true;
|
||||
}),
|
||||
hasDragoverFunc: Vue.computed(() => this.onDragover),
|
||||
mode: Vue.computed(() => this.mode)
|
||||
};
|
||||
},
|
||||
props: {
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'de'
|
||||
},
|
||||
timezone: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: [Date, String, Number, luxon.DateTime],
|
||||
default: luxon.DateTime.local()
|
||||
},
|
||||
modes: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: {}
|
||||
// TODO(chris): verfication functions
|
||||
},
|
||||
mode: String,
|
||||
modeOptions: Object,
|
||||
events: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
backgrounds: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
showBtns: Boolean,
|
||||
btnMonth: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
btnWeek: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
btnDay: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
btnList: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
timeGrid: Array,
|
||||
draggableEvents: [Boolean, Array, Function],
|
||||
dropableEvents: [Boolean, Array, Function],
|
||||
onDragover: Function,
|
||||
onDrop: Function
|
||||
},
|
||||
emits: [
|
||||
"click:next",
|
||||
"click:prev",
|
||||
"click:mode",
|
||||
"click:event",
|
||||
"click:day",
|
||||
"click:week",
|
||||
"update:date",
|
||||
"update:mode",
|
||||
"update:range",
|
||||
"drop"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
internalView: null,
|
||||
internalDate: null,
|
||||
modalEvent: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
convertedEvents() {
|
||||
return this.events.map(orig => ({
|
||||
id: orig.type + orig[orig.type + '_id'],
|
||||
type: orig.type,
|
||||
start: luxon.DateTime.fromISO(orig.isostart).setZone(this.timezone),
|
||||
end: luxon.DateTime.fromISO(orig.isoend).setZone(this.timezone),
|
||||
orig
|
||||
}));
|
||||
},
|
||||
convertedBackgrounds() {
|
||||
return this.backgrounds.map(bg => {
|
||||
const res = { ...bg };
|
||||
if (res.start) {
|
||||
if (Number.isInteger(res.start))
|
||||
res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
|
||||
else if (res.start instanceof Date)
|
||||
res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale });
|
||||
else if (typeof res.start ===
|
||||
'string' || res.start instanceof String)
|
||||
res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
|
||||
}
|
||||
if (res.end) {
|
||||
if (Number.isInteger(res.end))
|
||||
res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
|
||||
else if (res.end instanceof Date)
|
||||
res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale });
|
||||
else if (typeof res.end ===
|
||||
'string' || res.end instanceof String)
|
||||
res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
|
||||
}
|
||||
return res;
|
||||
});
|
||||
},
|
||||
cDate: {
|
||||
get() {
|
||||
if (this.internalDate) {
|
||||
return this.internalDate.setLocale(this.locale);
|
||||
}
|
||||
return luxon.DateTime.fromJSDate(new Date(this.date)).setZone(this.timezone).setLocale(this.locale);
|
||||
},
|
||||
set(value) {
|
||||
this.internalDate = value;
|
||||
this.$emit('update:date', value);
|
||||
}
|
||||
},
|
||||
cMode: {
|
||||
get() {
|
||||
if (!this.internalView) {
|
||||
// choose default mode
|
||||
let mode = this.mode;
|
||||
if (!mode || !this.modes[mode])
|
||||
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
|
||||
return mode || '';
|
||||
}
|
||||
return this.internalView;
|
||||
},
|
||||
set(value) {
|
||||
this.internalView = value;
|
||||
this.$emit('update:mode', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickPrev() {
|
||||
const evt = new Event('click:prev', {cancelable: true});
|
||||
this.$emit('click:prev', evt);
|
||||
if (evt.defaultPrevented)
|
||||
return;
|
||||
|
||||
// default: switch page
|
||||
this.$refs.mode.prevPage();
|
||||
},
|
||||
clickNext() {
|
||||
const evt = new Event('click:next', {cancelable: true});
|
||||
this.$emit('click:next', evt);
|
||||
if (evt.defaultPrevented)
|
||||
return;
|
||||
|
||||
// default: switch page
|
||||
this.$refs.mode.nextPage();
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
// TODO(chris): implement
|
||||
switch (evt.detail.source) {
|
||||
case 'day':
|
||||
if (this.cMode != 'day' && this.modes['day']) {
|
||||
evt.stopPropagation();
|
||||
this.cDate = evt.detail.value;
|
||||
this.cMode = 'day';
|
||||
}
|
||||
break;
|
||||
case 'week':
|
||||
if (this.cMode != 'week' && this.modes['week']) {
|
||||
evt.stopPropagation();
|
||||
this.cDate = luxon.DateTime.fromObject({
|
||||
localWeekNumber: evt.detail.value.number,
|
||||
localWeekYear: evt.detail.value.year
|
||||
}, {
|
||||
zone: this.cDate.zoneName,
|
||||
locale: this.cDate.locale
|
||||
});
|
||||
this.cMode = 'week';
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
onDropItem(evt, start, end) {
|
||||
this.$emit('drop', evt, start, end);
|
||||
},
|
||||
showEventModal(eventObj) {
|
||||
this.modalEvent = eventObj;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
hideEventModal() {
|
||||
if (this.modalEvent)
|
||||
this.modalEvent.closeFn = undefined;
|
||||
this.$refs.modal.hide();
|
||||
this.modalEvent = null;
|
||||
},
|
||||
onModalHidden() {
|
||||
if (this.modalEvent.closeFn)
|
||||
this.modalEvent.closeFn();
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-base h-100">
|
||||
<base-draganddrop
|
||||
class="card h-100"
|
||||
:events="convertedEvents"
|
||||
:backgrounds="convertedBackgrounds"
|
||||
@drop="onDropItem"
|
||||
v-cal-click:container
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-header
|
||||
class="card-header"
|
||||
v-model:date="cDate"
|
||||
v-model:mode="cMode"
|
||||
@prev="clickPrev"
|
||||
@next="clickNext"
|
||||
@click:mode="$emit('click:mode', $event)"
|
||||
:btn-day="!!modes['day'] && (btnDay || (showBtns && btnDay !== false))"
|
||||
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
|
||||
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
|
||||
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
|
||||
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</base-header>
|
||||
<component
|
||||
:is="modes ? modes[cMode] : null || 'div'"
|
||||
ref="mode"
|
||||
v-model:current-date="cDate"
|
||||
@update:range="$emit('update:range', $event)"
|
||||
@request-modal-open="showEventModal"
|
||||
@request-modal-close="hideEventModal"
|
||||
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
|
||||
>
|
||||
<template v-slot="slot"><slot v-bind="slot" /></template>
|
||||
</component>
|
||||
</base-draganddrop>
|
||||
<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}" />
|
||||
</template>
|
||||
<template #default>
|
||||
<slot v-if="modalEvent" v-bind="{mode: 'event', event: modalEvent.event}" />
|
||||
</template>
|
||||
</bs-modal>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
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) {
|
||||
DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
|
||||
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>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
import GridLine from './Grid/Line.js';
|
||||
import GridLineEvent from './Grid/Line/Event.js';
|
||||
|
||||
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
|
||||
|
||||
export default {
|
||||
name: "CalendarGrid",
|
||||
components: {
|
||||
GridLine,
|
||||
GridLineEvent
|
||||
},
|
||||
directives: {
|
||||
CalDnd
|
||||
},
|
||||
inject: {
|
||||
originalEvents: "events",
|
||||
originalBackgrounds: "backgrounds",
|
||||
dropAllowed: "dropAllowed"
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
flipAxis: Vue.computed(() => this.flipAxis),
|
||||
axisRow: Vue.computed(() => this.axisRow)
|
||||
};
|
||||
},
|
||||
props: {
|
||||
axisMain: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value.every(item => item instanceof luxon.DateTime);
|
||||
}
|
||||
},
|
||||
axisParts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value.every(item =>
|
||||
item instanceof luxon.Duration
|
||||
|| Number.isInteger(item)
|
||||
|| (
|
||||
(
|
||||
item.start instanceof luxon.Duration
|
||||
|| Number.isInteger(item.start)
|
||||
) && (
|
||||
item.end instanceof luxon.Duration
|
||||
|| Number.isInteger(item.end)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
flipAxis: Boolean,
|
||||
allDayEvents: Boolean,
|
||||
axisMainCollapsible: Boolean,
|
||||
snapToGrid: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
resizeObserver: null,
|
||||
mutationObserver: null,
|
||||
userScroll: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
axisRow() {
|
||||
return this.flipAxis ? 'column' : 'row';
|
||||
},
|
||||
axisCol() {
|
||||
return this.flipAxis ? 'row' : 'column';
|
||||
},
|
||||
axisPartsWithBreaks() {
|
||||
return this.axisParts.reduce((res, tu, index) => {
|
||||
const start = tu.start || tu;
|
||||
const end = tu.end;
|
||||
|
||||
if (res.length) {
|
||||
const lastTuEnd = res.pop();
|
||||
if (Array.isArray(lastTuEnd)) {
|
||||
res.push({
|
||||
start: lastTuEnd[0],
|
||||
end: start,
|
||||
index: lastTuEnd[1]
|
||||
});
|
||||
} else if (lastTuEnd != start) {
|
||||
// add pause
|
||||
res.push({
|
||||
start: lastTuEnd,
|
||||
end: start
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!end) {
|
||||
res.push([start, index]);
|
||||
} else {
|
||||
res.push({
|
||||
start,
|
||||
end,
|
||||
index
|
||||
});
|
||||
res.push(end);
|
||||
}
|
||||
return res;
|
||||
}, []).slice(0, -1);
|
||||
},
|
||||
axisPartsSave() {
|
||||
if (!this.axisParts[this.axisParts.length - 1].end)
|
||||
return this.axisParts.slice(0, -1);
|
||||
return this.axisParts;
|
||||
},
|
||||
start() {
|
||||
return this.axisPartsWithBreaks[0].start;
|
||||
},
|
||||
end() {
|
||||
return this.axisPartsWithBreaks[this.axisPartsWithBreaks.length - 1].end;
|
||||
},
|
||||
ends() {
|
||||
const ends = [];
|
||||
const partsEnds = this.axisPartsWithBreaks
|
||||
.filter(p => p.index !== undefined)
|
||||
.map(p => p.end);
|
||||
for (var date of this.axisMain)
|
||||
for (var part of partsEnds)
|
||||
ends.push(date.plus(part));
|
||||
|
||||
return ends;
|
||||
},
|
||||
axisMainBorders() {
|
||||
return this.axisMain.reduce(
|
||||
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
|
||||
[]
|
||||
);
|
||||
},
|
||||
eventsAllDay() {
|
||||
if (!this.allDayEvents)
|
||||
return [];
|
||||
return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
|
||||
},
|
||||
eventsNormal() {
|
||||
if (!this.allDayEvents)
|
||||
return this.events;
|
||||
return this.mapIntoMainAxis(this.originalEvents.filter(event => !event.orig.allDayEvent));
|
||||
},
|
||||
events() {
|
||||
return this.mapIntoMainAxis(this.originalEvents);
|
||||
},
|
||||
backgrounds() {
|
||||
return this.mapIntoMainAxis(this.originalBackgrounds);
|
||||
},
|
||||
hasValidEvents() {
|
||||
return this.events.find(e => e.length);
|
||||
},
|
||||
styleGridCols() {
|
||||
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
|
||||
if (this.axisMainCollapsible) {
|
||||
if (this.hasValidEvents)
|
||||
cols = this.events
|
||||
.map(e => e.length
|
||||
? '1fr'
|
||||
: 'var(--fhc-calendar-axis-collapsible, .5fr)')
|
||||
.join(' ');
|
||||
}
|
||||
return cols;
|
||||
},
|
||||
styleGridRows() {
|
||||
const gridlines = {};
|
||||
|
||||
this.axisPartsWithBreaks.forEach(part => {
|
||||
let ts = part.start.toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts];
|
||||
if (part.index !== undefined)
|
||||
gridlines[ts].push('ps_' + part.index);
|
||||
ts = part.end.toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts];
|
||||
if (part.index !== undefined)
|
||||
gridlines[ts].push('pe_' + part.index);
|
||||
});
|
||||
|
||||
this.eventsNormal.forEach((events, mainIndex) => {
|
||||
let day = this.axisMain[mainIndex];
|
||||
events.forEach(event => {
|
||||
if (!event.startsHere && !event.endsHere)
|
||||
return;
|
||||
|
||||
if (event.startsHere) {
|
||||
let ts = event.start.diff(day).toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts, 'e_' + ts];
|
||||
}
|
||||
if (event.endsHere) {
|
||||
let ts = event.end.diff(day).toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts, 'e_' + ts];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Object.keys(gridlines).sort((a,b) => parseInt(a)-parseInt(b)).map((start, i, keys) => {
|
||||
let end = keys[i + 1];
|
||||
if (!end) {
|
||||
gridlines[start].push('end');
|
||||
return '[' + gridlines[start].join(' ') + ']';
|
||||
}
|
||||
return '[' + gridlines[start].join(' ') + '] ' + (end - start) + 'fr';
|
||||
}).join(' ');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapIntoMainAxis(target) {
|
||||
const result = Array.from({length: this.axisMain.length}, () => Array());
|
||||
|
||||
target.forEach(event => {
|
||||
const start = event.start || this.axisMainBorders[0].plus(-1);
|
||||
const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1);
|
||||
|
||||
for (var i = 0; i < this.axisMain.length; i++) {
|
||||
if (start < this.axisMainBorders[i * 2 + 1] && end > this.axisMainBorders[i * 2]) {
|
||||
const startsHere = start >= this.axisMainBorders[i * 2];
|
||||
const endsHere = end <= this.axisMainBorders[i * 2 + 1];
|
||||
result[i].push({
|
||||
...event,
|
||||
startsHere,
|
||||
endsHere
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/* DRAG AND DROP */
|
||||
getPageTop(el) {
|
||||
let pageTop = el.offsetTop;
|
||||
if (el.offsetParent)
|
||||
pageTop += this.getPageTop(el.offsetParent);
|
||||
return pageTop;
|
||||
},
|
||||
getPageLeft(el) {
|
||||
let pageLeft = el.offsetLeft;
|
||||
if (el.offsetParent)
|
||||
pageLeft += this.getPageLeft(el.offsetParent);
|
||||
return pageLeft;
|
||||
},
|
||||
getTimestampFromMouse(evt, dayTimestamp) {
|
||||
let mouse, mouseFrac;
|
||||
if (this.flipAxis) {
|
||||
mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.main.scrollLeft;
|
||||
mouseFrac = mouse / this.$refs.body.offsetWidth;
|
||||
} else {
|
||||
mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.main.scrollTop;
|
||||
mouseFrac = mouse / this.$refs.body.offsetHeight;
|
||||
}
|
||||
|
||||
return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
|
||||
},
|
||||
|
||||
/* SCROLLING */
|
||||
enableAutoScroll() {
|
||||
if (!this.resizeObserver)
|
||||
this.resizeObserver = new ResizeObserver(this.scrollToEarliestEvent);
|
||||
this.resizeObserver.observe(this.$refs.body);
|
||||
|
||||
if (!this.mutationObserver)
|
||||
this.mutationObserver = new MutationObserver(mutations => {
|
||||
if (mutations.some(m => m.addedNodes.length && [].some.call(m.addedNodes, el => el.matches && el.matches('.fhc-calendar-base-grid-line-event'))))
|
||||
this.scrollToEarliestEvent();
|
||||
});
|
||||
this.mutationObserver.observe(this.$refs.body, {
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
|
||||
this.scrollToEarliestEvent();
|
||||
},
|
||||
disableAutoScroll() {
|
||||
if (this.resizeObserver)
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
|
||||
if (this.mutationObserver)
|
||||
this.mutationObserver.disconnect();
|
||||
this.mutationObserver = null;
|
||||
},
|
||||
scrollToEarliestEvent() {
|
||||
const eventElements = this.$refs.scroller.querySelectorAll('.fhc-calendar-base-grid-line-event');
|
||||
const earliestEventOffset = eventElements.values()
|
||||
.reduce((res, el) => {
|
||||
const top = el.offsetTop;
|
||||
if (!res[1] || top < res[0])
|
||||
return [top, el];
|
||||
return res;
|
||||
}, [0, null]);
|
||||
|
||||
this.userScroll = false;
|
||||
if (earliestEventOffset[1]) {
|
||||
earliestEventOffset[1].scrollIntoView({ behavior: "smooth" });
|
||||
} else {
|
||||
this.$refs.scroller.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.disableAutoScroll();
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid"
|
||||
style="display:grid;width:100%;height:100%"
|
||||
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
|
||||
>
|
||||
<div
|
||||
class="grid-header"
|
||||
style="display:grid"
|
||||
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
|
||||
>
|
||||
<div
|
||||
v-for="(date, index) in axisMain"
|
||||
:key="index"
|
||||
class="main-header"
|
||||
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}"
|
||||
:style="'grid-' + axisCol + ':' + (2+index)"
|
||||
>
|
||||
<slot name="main-header" v-bind="{ index, date }" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="allDayEvents"
|
||||
class="grid-allday"
|
||||
style="display:grid"
|
||||
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
|
||||
>
|
||||
<div
|
||||
v-for="(events, index) in eventsAllDay"
|
||||
:key="index"
|
||||
class="all-day-events"
|
||||
:style="'grid-' + axisCol + ':' + (2+index)"
|
||||
>
|
||||
<grid-line-event
|
||||
v-for="(event, i) in events"
|
||||
:key="i"
|
||||
:event="event"
|
||||
>
|
||||
<template v-slot="slot">
|
||||
<slot name="event" v-bind="slot" />
|
||||
</template>
|
||||
</grid-line-event>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="scroller"
|
||||
@scrollend="userScroll ? disableAutoScroll() : userScroll = true"
|
||||
style="display:grid;overflow:auto"
|
||||
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
|
||||
>
|
||||
<div
|
||||
ref="main"
|
||||
class="grid-main"
|
||||
style="position:relative;grid-column:1/-1;grid-row:1/-1;display:grid"
|
||||
:style="'grid-template-' + axisCol + 's:subgrid;grid-template-' + axisRow + 's:' + styleGridRows"
|
||||
>
|
||||
<div
|
||||
v-for="(part, index) in axisPartsSave"
|
||||
:key="index"
|
||||
class="part-header"
|
||||
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
|
||||
>
|
||||
<slot name="part-header" v-bind="{ index, part }" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="body"
|
||||
class="grid-body"
|
||||
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid"
|
||||
: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"
|
||||
:key="index"
|
||||
>
|
||||
<div
|
||||
v-for="(part, i) in axisPartsSave"
|
||||
:key="i"
|
||||
class="part-body"
|
||||
style="position:relative"
|
||||
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
|
||||
>
|
||||
<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))}"
|
||||
></div>
|
||||
</div>
|
||||
<grid-line
|
||||
:start="date.plus(start)"
|
||||
:end="date.plus(end)"
|
||||
:date="date"
|
||||
:events="eventsNormal[index]"
|
||||
:backgrounds="backgrounds[index]"
|
||||
style="position:relative"
|
||||
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
|
||||
>
|
||||
<template #event="slot">
|
||||
<slot name="event" v-bind="slot" />
|
||||
</template>
|
||||
<template #dropzone>
|
||||
<div
|
||||
v-if="!snapToGrid && dragging"
|
||||
style="position:absolute;inset:0;z-index:1"
|
||||
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
|
||||
></div>
|
||||
</template>
|
||||
</grid-line>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import LineEvent from './Line/Event.js';
|
||||
import LineBackground from './Line/Background.js';
|
||||
|
||||
/**
|
||||
* TODO(chris):
|
||||
* Event overflow for Month mode (more-button)
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "GridLine",
|
||||
components: {
|
||||
LineEvent,
|
||||
LineBackground
|
||||
},
|
||||
inject: {
|
||||
axisRow: "axisRow"
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
start: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
end: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
events: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
backgrounds: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
eventsWithRowInfo() {
|
||||
const events = [];
|
||||
this.events.forEach(event => {
|
||||
const rows = [1, -1];
|
||||
if (event.startsHere) {
|
||||
rows[0] = 't_' + event.start.diff(this.date).toMillis();
|
||||
}
|
||||
if (event.endsHere) {
|
||||
rows[1] = 't_' + event.end.diff(this.date).toMillis();
|
||||
}
|
||||
|
||||
events.push({
|
||||
...event,
|
||||
rows
|
||||
});
|
||||
});
|
||||
return events;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line"
|
||||
style="position:relative;display:grid;grid-auto-flow:dense"
|
||||
:style="'grid-template-' + axisRow + 's:subgrid'"
|
||||
>
|
||||
<line-background
|
||||
v-for="bg in backgrounds"
|
||||
:start="start"
|
||||
:end="end"
|
||||
:background="bg"
|
||||
></line-background>
|
||||
<line-event
|
||||
v-for="(event, i) in eventsWithRowInfo"
|
||||
:key="i"
|
||||
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
|
||||
:event="event"
|
||||
>
|
||||
<template v-slot="slot">
|
||||
<slot name="event" v-bind="slot" />
|
||||
</template>
|
||||
</line-event>
|
||||
<slot name="dropzone" />
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
export default {
|
||||
name: "GridLineBackground",
|
||||
inject: {
|
||||
flipAxis: "flipAxis"
|
||||
},
|
||||
props: {
|
||||
start: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
end: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
background: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator(value) {
|
||||
if (!value.start && !value.end)
|
||||
return false;
|
||||
if (value.start && !(value.start instanceof luxon.DateTime))
|
||||
return false;
|
||||
if (value.end && !(value.end instanceof luxon.DateTime))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
if (!this.background.endsHere && !this.background.startsHere)
|
||||
return this.background.style;
|
||||
|
||||
const perc = (this.end.ts - this.start.ts) / 100;
|
||||
|
||||
let border = {};
|
||||
if (this.background.startsHere)
|
||||
border[this.flipAxis ? 'left' : 'top'] = (this.background.start.diff(this.start)) / perc + '%';
|
||||
if (this.background.endsHere)
|
||||
border[this.flipAxis ? 'right' : 'bottom'] = (this.end.diff(this.background.end)) / perc + '%';
|
||||
|
||||
if (!this.background.style)
|
||||
return border;
|
||||
|
||||
return [this.background.style, border];
|
||||
},
|
||||
classes() {
|
||||
if (!this.background.endsHere && !this.background.startsHere)
|
||||
return this.background.class;
|
||||
|
||||
const result = [];
|
||||
if (this.background.class)
|
||||
result.push(this.background.class);
|
||||
if (this.background.startsHere)
|
||||
result.push('bg-begin');
|
||||
if (this.background.endsHere)
|
||||
result.push('bg-end');
|
||||
return result;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line-background"
|
||||
:class="classes"
|
||||
style="position:absolute;inset:0;z-index:0"
|
||||
:style="styles"
|
||||
:title="background.title"
|
||||
>
|
||||
<span v-if="background.label">{{ background.label }}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
|
||||
import CalClick from '../../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "GridLineEvent",
|
||||
directives: {
|
||||
CalDnd,
|
||||
CalClick
|
||||
},
|
||||
inject: {
|
||||
draggableEvents: "draggableEvents",
|
||||
mode: "mode"
|
||||
},
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return (value.start && value.end && value.orig);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isHeaderOrFooter() {
|
||||
return ['header', 'footer'].includes(this.event.orig);
|
||||
},
|
||||
draggable() {
|
||||
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
|
||||
},
|
||||
classes() {
|
||||
const classes = [];
|
||||
if (this.isHeaderOrFooter) {
|
||||
classes.push('event-' + this.event.orig);
|
||||
} else {
|
||||
if (this.event.startsHere)
|
||||
classes.push('event-begin');
|
||||
if (this.event.endsHere)
|
||||
classes.push('event-end');
|
||||
}
|
||||
return classes
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line-event event"
|
||||
:class="classes"
|
||||
style="z-index: 1"
|
||||
:draggable="draggable"
|
||||
v-cal-dnd:draggable="event"
|
||||
v-cal-click:event="isHeaderOrFooter ? event : event.orig"
|
||||
>
|
||||
<slot :event="isHeaderOrFooter ? event : event.orig">
|
||||
{{ event.orig }}
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* TODO(chris): use click-directive
|
||||
*/
|
||||
import DatePicker from './Header/Datepicker.js';
|
||||
|
||||
export default {
|
||||
name: "CalendarHeader",
|
||||
components: {
|
||||
DatePicker
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modeOptions: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
btnMonth: Boolean,
|
||||
btnWeek: Boolean,
|
||||
btnDay: Boolean,
|
||||
btnList: Boolean
|
||||
},
|
||||
emits: [
|
||||
"next",
|
||||
"prev",
|
||||
"click:mode",
|
||||
"update:date",
|
||||
"update:mode"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
open: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickMode(evt, mode) {
|
||||
this.$emit('click:mode', evt);
|
||||
if (!evt.defaultPrevented)
|
||||
this.$emit('update:mode', mode);
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-base-header">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="col-auto d-flex justify-content-center">
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
class="btn btn-outline-secondary border-0"
|
||||
@click="$emit('prev')"
|
||||
:disabled="open"
|
||||
>
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<date-picker
|
||||
:mode="mode"
|
||||
:date="date"
|
||||
@update:date="$emit('update:date', $event)"
|
||||
@open="open = true"
|
||||
@closed="open = false"
|
||||
:list-length="modeOptions.length"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary border-0"
|
||||
@click="$emit('next')"
|
||||
:disabled="open"
|
||||
>
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-flex gap-1 justify-content-end" role="group">
|
||||
<button
|
||||
v-if="btnMonth"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'month'}"
|
||||
@click="clickMode($event, 'month')"
|
||||
>
|
||||
<i class="fa fa-calendar-days"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="btnWeek"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'week'}"
|
||||
@click="clickMode($event, 'week')"
|
||||
>
|
||||
<i class="fa fa-calendar-week"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="btnDay"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'day'}"
|
||||
@click="clickMode($event, 'day')"
|
||||
>
|
||||
<i class="fa fa-calendar-day"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="btnList"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'list'}"
|
||||
@click="clickMode($event, 'list')"
|
||||
>
|
||||
<i class="fa fa-table-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// TODO(chris): translate aria-labels
|
||||
|
||||
export default {
|
||||
name: "CalendarHeaderDatepicker",
|
||||
components: {
|
||||
VueDatePicker
|
||||
},
|
||||
inject: [
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
listLength: {
|
||||
type: Number,
|
||||
default: 7
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:date"
|
||||
],
|
||||
computed: {
|
||||
convertedDate() {
|
||||
// convert to target TZ then strip TZ Information
|
||||
// so the datepicker can work with local times
|
||||
return this.date.setZone(this.timezone).setZone('local', { keepLocalTime: true });
|
||||
},
|
||||
current() {
|
||||
switch (this.mode) {
|
||||
case "month":
|
||||
return {month: this.convertedDate.month-1, year: this.convertedDate.year};
|
||||
case "list":
|
||||
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
|
||||
case "week":
|
||||
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
|
||||
case "day":
|
||||
return this.convertedDate;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
title() {
|
||||
switch (this.mode) {
|
||||
case "month":
|
||||
return this.date.toLocaleString({ month: 'long', year: 'numeric' });
|
||||
case "week":
|
||||
var year = this.date.localWeekYear;
|
||||
var week = this.date.toFormat('nn');
|
||||
return this.$p.t('calendar/year_kw', { year, week });
|
||||
case "list":
|
||||
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
|
||||
case "day":
|
||||
return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
|
||||
default:
|
||||
return 'View not Supported';
|
||||
}
|
||||
},
|
||||
weekStart() {
|
||||
return luxon.Info.getStartOfWeek(this.date)%7;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update(value) {
|
||||
let date;
|
||||
switch (this.mode) {
|
||||
case "month":
|
||||
value.month++;
|
||||
date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
|
||||
break;
|
||||
case "list":
|
||||
case "week":
|
||||
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
|
||||
break;
|
||||
case "day":
|
||||
date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
|
||||
break;
|
||||
default:
|
||||
return; // Don't update if the value is invalid!
|
||||
}
|
||||
this.$emit("update:date", date);
|
||||
},
|
||||
weekNumbers(date) {
|
||||
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<vue-date-picker
|
||||
:model-value="current"
|
||||
@update:model-value="update"
|
||||
:format="() => title"
|
||||
:month-picker="mode == 'month'"
|
||||
:week-picker="mode == 'week'"
|
||||
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
|
||||
:text-input="mode == 'day'"
|
||||
:week-start="weekStart"
|
||||
:week-numbers="{ type: weekNumbers }"
|
||||
:clearable="false"
|
||||
:enable-time-picker="false"
|
||||
:config="{ keepActionRow: mode != 'month' }"
|
||||
:action-row="{ showSelect: false, showCancel: false, showNow: mode != 'month', showPreview: false }"
|
||||
auto-apply
|
||||
six-weeks
|
||||
teleport
|
||||
:locale="locale"
|
||||
:now-button-label="$p.t('calendar/today')"
|
||||
:week-num-name="$p.t('calendar/kw')"
|
||||
/>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "LabelDay",
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
titleFull() {
|
||||
return this.date.toLocaleString({day: 'numeric', month: 'long', year: 'numeric'});
|
||||
},
|
||||
titleLong() {
|
||||
return this.date.toLocaleString({day: '2-digit', month: '2-digit', year: 'numeric'});
|
||||
},
|
||||
titleShort() {
|
||||
return this.date.toLocaleString({day: 'numeric', month: 'numeric'});
|
||||
},
|
||||
titleNarrow() {
|
||||
return this.date.toLocaleString({day: 'numeric'});
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-label-day"
|
||||
v-cal-click:day="date"
|
||||
>
|
||||
<span class="full">{{ titleFull }}</span>
|
||||
<span class="long">{{ titleLong }}</span>
|
||||
<span class="short">{{ titleShort }}</span>
|
||||
<span class="narrow">{{ titleNarrow }}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "LabelDow",
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
titleLong() {
|
||||
return this.date.toLocaleString({weekday: 'long'});
|
||||
},
|
||||
titleShort() {
|
||||
return this.date.toLocaleString({weekday: 'short'});
|
||||
},
|
||||
titleNarrow() {
|
||||
return this.date.toLocaleString({weekday: 'narrow'});
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-label-dow"
|
||||
v-cal-click:dow="date"
|
||||
>
|
||||
<b class="long">{{ titleLong }}</b>
|
||||
<b class="short">{{ titleShort }}</b>
|
||||
<b class="narrow">{{ titleNarrow }}</b>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export default {
|
||||
name: "LabelTime",
|
||||
props: {
|
||||
part: {
|
||||
type: [luxon.Duration, Number, Object],
|
||||
required: true,
|
||||
validator(value) {
|
||||
if (value instanceof Object) {
|
||||
if (value instanceof luxon.Duration)
|
||||
return true;
|
||||
let start_ok = true;
|
||||
let end_ok = true;
|
||||
if (value.start) {
|
||||
start_ok = (
|
||||
value.start instanceof luxon.Duration
|
||||
|| Number.isInteger(value.start)
|
||||
);
|
||||
}
|
||||
if (value.end) {
|
||||
end_ok = (
|
||||
value.end instanceof luxon.Duration
|
||||
|| Number.isInteger(value.end)
|
||||
);
|
||||
}
|
||||
return start_ok && end_ok;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sanitizedTimestamps() {
|
||||
return this.part.start || this.part.end ? this.part : { start: this.part };
|
||||
},
|
||||
start() {
|
||||
if (!this.sanitizedTimestamps.start)
|
||||
return null;
|
||||
return this.formatTime(this.sanitizedTimestamps.start);
|
||||
},
|
||||
end() {
|
||||
if (!this.sanitizedTimestamps.end)
|
||||
return null;
|
||||
return this.formatTime(this.sanitizedTimestamps.end);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatTime(date) {
|
||||
return date.toISOTime({ suppressSeconds: true });
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-base-label-time">
|
||||
<span v-if="start">{{ start }}</span>
|
||||
<span v-if="end">-</span>
|
||||
<span v-if="end">{{ end }}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "LabelWeek",
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
weeks() {
|
||||
const firstDay = this.date.startOf('week', { useLocaleWeeks: true });
|
||||
const lastDay = this.date.endOf('week', { useLocaleWeeks: true });
|
||||
|
||||
const weeks = [
|
||||
{ number: firstDay.localWeekNumber, year: firstDay.localWeekYear },
|
||||
{ number: lastDay.localWeekNumber, year: lastDay.localWeekYear }
|
||||
];
|
||||
if (weeks[0].number == weeks[1].number)
|
||||
weeks.pop();
|
||||
return weeks;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-base-label-week">
|
||||
<span
|
||||
v-for="week in weeks"
|
||||
v-cal-click:week="week"
|
||||
>
|
||||
{{ week.number }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
export default {
|
||||
name: 'CalendarSlider',
|
||||
inject: {
|
||||
time: {
|
||||
from: "sliderTime",
|
||||
default: ".3s"
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'slid'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
target: 0,
|
||||
extrasAfter: 0,
|
||||
extrasBefore: 0,
|
||||
running: false,
|
||||
promiseResolve: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
itemsAfter() {
|
||||
return [...Array(this.extrasAfter)].map((i, k) => 1+k);
|
||||
},
|
||||
itemsBefore() {
|
||||
return [...Array(this.extrasBefore)].map((i, k) => k-this.extrasBefore);
|
||||
},
|
||||
styleSlider() {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
if (this.running) {
|
||||
style.left = (-this.target * 100) + '%';
|
||||
style.transition = 'left ' + this.time + ' ease-in-out';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
styleBefore() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
right: '100%',
|
||||
width: (this.extrasBefore * 100) + '%'
|
||||
};
|
||||
},
|
||||
styleAfter() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
left: '100%',
|
||||
width: (this.extrasAfter * 100) + '%'
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
return this.slidePages(-1);
|
||||
},
|
||||
nextPage() {
|
||||
return this.slidePages(1);
|
||||
},
|
||||
slidePages(dir) {
|
||||
return new Promise(resolve => {
|
||||
this.promiseResolve = resolve;
|
||||
this.running = true;
|
||||
const newTarget = this.target + dir;
|
||||
if (newTarget > 0) {
|
||||
if (this.extrasAfter < newTarget)
|
||||
this.extrasAfter = newTarget;
|
||||
} else if (newTarget < 0) {
|
||||
if (-this.extrasBefore > newTarget)
|
||||
this.extrasBefore = -newTarget;
|
||||
}
|
||||
this.target = newTarget;
|
||||
});
|
||||
},
|
||||
endSlide() {
|
||||
if (this.promiseResolve) {
|
||||
this.promiseResolve(this.target);
|
||||
this.promiseResolve = null;
|
||||
}
|
||||
this.$emit('slid', this.target);
|
||||
this.running = false;
|
||||
this.target = 0;
|
||||
this.extrasAfter = this.extrasBefore = 0;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-slider h-100"
|
||||
style="position:relative;overflow:hidden"
|
||||
>
|
||||
<div
|
||||
:style="styleSlider"
|
||||
@transitionend="endSlide"
|
||||
>
|
||||
<div :style="styleBefore">
|
||||
<div
|
||||
v-for="i in itemsBefore"
|
||||
:key="i"
|
||||
style="height:100%;width:100%"
|
||||
>
|
||||
<slot :offset="i" />
|
||||
</div>
|
||||
</div>
|
||||
<div :style="styleAfter">
|
||||
<div
|
||||
v-for="i in itemsAfter"
|
||||
:key="i"
|
||||
style="height:100%;width:100%"
|
||||
>
|
||||
<slot :offset="i" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:100%;width:100%">
|
||||
<slot :offset="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import DayView from './Day/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeDay",
|
||||
components: {
|
||||
BaseSlider,
|
||||
DayView
|
||||
},
|
||||
props: {
|
||||
currentDate: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:currentDate",
|
||||
"update:range",
|
||||
"click",
|
||||
"requestModalOpen",
|
||||
"requestModalClose"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
focusDate: this.currentDate,
|
||||
rangeOffset: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
let first = this.focusDate.startOf('day');
|
||||
let last = this.focusDate.endOf('day');
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
if (this.rangeOffset < 0) {
|
||||
first = first.plus({ days: this.rangeOffset });
|
||||
} else {
|
||||
last = last.plus({ days: this.rangeOffset });
|
||||
}
|
||||
}
|
||||
|
||||
return luxon.Interval.fromDateTimes(first, last);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentDate() {
|
||||
this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
|
||||
if (this.rangeOffset) {
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
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.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.prevPage().then(this.updatePage);
|
||||
},
|
||||
nextPage() {
|
||||
this.rangeOffset = this.$refs.slider.target + 1;
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.nextPage().then(this.updatePage);
|
||||
},
|
||||
updatePage(days) {
|
||||
const newFocusDate = this.focusDate.plus({ days });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
viewAttrs(days) {
|
||||
const day = this.focusDate.plus({ days });
|
||||
return { ...this.$attrs, day };
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="fhc-calendar-mode-day flex-grow-1 position-relative"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<day-view
|
||||
ref="view"
|
||||
v-bind="viewAttrs(slot.offset)"
|
||||
@request-modal-open="$emit('requestModalOpen', $event)"
|
||||
@request-modal-close="$emit('requestModalClose', $event)"
|
||||
>
|
||||
<template v-slot="slot"><slot v-bind="slot" /></template>
|
||||
</day-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import CalendarGrid from '../../Base/Grid.js';
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
import LabelTime from '../../Base/Label/Time.js';
|
||||
|
||||
export default {
|
||||
name: "DayView",
|
||||
components: {
|
||||
CalendarGrid,
|
||||
LabelDay,
|
||||
LabelDow,
|
||||
LabelTime
|
||||
},
|
||||
inject: {
|
||||
timeGrid: "timeGrid",
|
||||
originalEvents: "events",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
emptyMessage: String,
|
||||
emptyMessageDetails: String,
|
||||
compact: Boolean
|
||||
},
|
||||
emits: [
|
||||
"requestModalOpen",
|
||||
"requestModalClose"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
chosenEvent: null,
|
||||
gridMainRef: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
axisMain() {
|
||||
return [this.day.startOf('day')];
|
||||
},
|
||||
axisParts() {
|
||||
if (this.timeGrid) {
|
||||
// create {start, end} array
|
||||
return this.timeGrid.map(tu => {
|
||||
return {
|
||||
start: luxon.Duration.fromISOTime(tu.start),
|
||||
end: luxon.Duration.fromISOTime(tu.end)
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// create 07:00-23:00
|
||||
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return this.originalEvents
|
||||
.filter(event => event.start < this.day.plus({ days: 1 }) && event.end > this.day)
|
||||
.sort((a, b) => a.start.ts - b.start.ts)
|
||||
.map(evt => evt.orig);
|
||||
},
|
||||
currentEvent() {
|
||||
if (this.chosenEvent) {
|
||||
if (this.events.find(e => e == this.chosenEvent))
|
||||
return this.chosenEvent;
|
||||
}
|
||||
let first = null;
|
||||
if (this.events)
|
||||
first = this.events.find(Boolean); // undefined => none found
|
||||
|
||||
if (first && first.type == 'loading')
|
||||
return null; // null => loading
|
||||
|
||||
return first;
|
||||
},
|
||||
isToday() {
|
||||
return this.day.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
compact() {
|
||||
if (this.compact) {
|
||||
if (this.chosenEvent) {
|
||||
this.$emit('requestModalOpen', {
|
||||
event: this.chosenEvent,
|
||||
closeFn: () => { this.chosenEvent = null; }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.$emit('requestModalClose');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickDefaults(evt) {
|
||||
if (evt.detail.source == 'event') {
|
||||
this.chosenEvent = evt.detail.value;
|
||||
if (this.compact) {
|
||||
this.$emit('requestModalOpen', {
|
||||
event: this.chosenEvent,
|
||||
closeFn: () => { this.chosenEvent = null; }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.gridMainRef = this.$refs.grid.$refs.main;
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-mode-day-view d-flex h-100"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<calendar-grid
|
||||
ref="grid"
|
||||
:axis-main="axisMain"
|
||||
:axis-parts="axisParts"
|
||||
:snap-to-grid="!!timeGrid"
|
||||
all-day-events
|
||||
>
|
||||
<template #main-header="{ date }">
|
||||
<div :class="{ today: isToday }">
|
||||
<label-dow
|
||||
@cal-click="evt => evt.detail.source = 'day'"
|
||||
v-bind="{ date }"
|
||||
/>
|
||||
<label-day
|
||||
v-bind="{ date }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #part-header="{ part }">
|
||||
<label-time v-bind="{ part }" />
|
||||
</template>
|
||||
<template #event="slot">
|
||||
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
|
||||
<span class="placeholder w-100 h-100" />
|
||||
</div>
|
||||
<slot v-else v-bind="slot" mode="day" />
|
||||
</template>
|
||||
</calendar-grid>
|
||||
<Teleport :disabled="!gridMainRef" :to="gridMainRef">
|
||||
<div
|
||||
v-if="emptyMessage && currentEvent !== null && !currentEvent"
|
||||
class="fhc-calendar-no-events-overlay"
|
||||
style="position:absolute;inset:0"
|
||||
>
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
</Teleport>
|
||||
<div class="event-details" v-if="!compact">
|
||||
<div
|
||||
v-if="currentEvent === null"
|
||||
class="p-4 d-flex w-100 justify-content-center align-items-center"
|
||||
>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<h3 v-else-if="!currentEvent">{{ emptyMessageDetails }}</h3>
|
||||
<slot v-else :event="currentEvent" mode="event" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import ListView from './List/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeList",
|
||||
components: {
|
||||
BaseSlider,
|
||||
ListView
|
||||
},
|
||||
props: {
|
||||
currentDate: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
default: 7
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:currentDate",
|
||||
"update:range",
|
||||
"click",
|
||||
"requestModalOpen"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
focusDate: this.currentDate,
|
||||
rangeOffset: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
let first = this.focusDate;
|
||||
let last = first.plus({ days: this.length });
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
if (this.rangeOffset < 0) {
|
||||
first = first.plus({ days: this.rangeOffset });
|
||||
} else {
|
||||
last = first.plus({ days: this.rangeOffset + this.length });
|
||||
}
|
||||
}
|
||||
|
||||
return luxon.Interval.fromDateTimes(first, last);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentDate() {
|
||||
this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
|
||||
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(offset) {
|
||||
const newFocusDate = this.focusDate.plus({ days: offset });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
},
|
||||
viewAttrs(offset) {
|
||||
const day = this.focusDate.plus({ days: offset });
|
||||
return { day, length: this.length };
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
switch (evt.detail.source) {
|
||||
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-list flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<list-view v-bind="viewAttrs(slot.offset)">
|
||||
<template v-slot="slot"><slot v-bind="slot" /></template>
|
||||
</list-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
|
||||
import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
// TODO(chris): drag and drop
|
||||
|
||||
export default {
|
||||
name: "ListView",
|
||||
components: {
|
||||
LabelDay,
|
||||
LabelDow
|
||||
},
|
||||
directives: {
|
||||
CalDnd,
|
||||
CalClick
|
||||
},
|
||||
inject: {
|
||||
draggableEvents: "draggableEvents",
|
||||
events: "events",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chosenEvent: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
days() {
|
||||
return Array.from({ length: this.length }, (e, days) => this.day.plus({ days }));
|
||||
},
|
||||
eventsPerDay() {
|
||||
const eventsPerDay = this.days.map(day => {
|
||||
return {
|
||||
day,
|
||||
events: this.events
|
||||
.filter(event => event.start < day.plus({ days: 1 }) && event.end > day)
|
||||
.sort((a, b) => a.start.ts - b.start.ts)
|
||||
};
|
||||
});
|
||||
return eventsPerDay.filter(day => day.events.length);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
draggable(event) {
|
||||
return this.draggableEvents(event.orig, 'list');
|
||||
},
|
||||
isToday(date) {
|
||||
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-mode-list-view h-100 overflow-auto">
|
||||
<div v-if="!eventsPerDay.length" class="h-100">
|
||||
<slot :event="undefined" mode="list" />
|
||||
</div>
|
||||
<div v-for="{ day, events } in eventsPerDay">
|
||||
<div
|
||||
class="text-center"
|
||||
:class="{ today: isToday(day) }"
|
||||
>
|
||||
<label-dow
|
||||
:date="day"
|
||||
class="d-inline"
|
||||
@cal-click="evt => evt.detail.source = 'day'"
|
||||
/>
|
||||
,
|
||||
<label-day :date="day" class="d-inline" />
|
||||
</div>
|
||||
<div v-for="event in events">
|
||||
<div v-if="event.type == 'loading'" class="placeholder-glow opacity-50">
|
||||
<span class="placeholder w-100" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="event"
|
||||
:draggable="draggable(event)"
|
||||
v-cal-dnd:draggable="event"
|
||||
v-cal-click:event="event.orig"
|
||||
>
|
||||
<slot :event="event.orig" mode="list" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import MonthView from './Month/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeMonth",
|
||||
components: {
|
||||
BaseSlider,
|
||||
MonthView
|
||||
},
|
||||
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('month').startOf('week', { useLocaleWeeks: true });
|
||||
let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset});
|
||||
const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
|
||||
if (this.rangeOffset < 0) {
|
||||
first = nextRangeStart;
|
||||
} else {
|
||||
last = nextRangeStart.plus({ days: 41 }).endOf('day');
|
||||
}
|
||||
}
|
||||
|
||||
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('month').diff(this.focusDate.startOf('month'), 'months').months;
|
||||
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(months) {
|
||||
const newFocusDate = this.focusDate.plus({ months });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
},
|
||||
viewAttrs(months) {
|
||||
const day = this.focusDate.plus({ months });
|
||||
return { day };
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
switch (evt.detail.source) {
|
||||
case 'week':
|
||||
// default: Move to week if not in month
|
||||
let dayInWeek = luxon.DateTime.fromObject({
|
||||
localWeekNumber: evt.detail.value.number,
|
||||
localWeekYear: evt.detail.value.year
|
||||
}, {
|
||||
zone: this.currentDate.zoneName,
|
||||
locale: this.currentDate.locale
|
||||
});
|
||||
|
||||
if (!this.focusDate.hasSame(dayInWeek.startOf('week', { useLocaleWeeks: true }), 'month')) {
|
||||
this.$emit('update:currentDate', dayInWeek.startOf('week', { useLocaleWeeks: true }));
|
||||
} else if (!this.focusDate.hasSame(dayInWeek.endOf('week', { useLocaleWeeks: true }), 'month')) {
|
||||
this.$emit('update:currentDate', dayInWeek.endOf('week', { useLocaleWeeks: true }));
|
||||
}
|
||||
break;
|
||||
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-month flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<month-view v-bind="viewAttrs(slot.offset)">
|
||||
<template v-slot="slot"><slot v-bind="slot" mode="month" /></template>
|
||||
</month-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import CalendarGrid from '../../Base/Grid.js';
|
||||
import LabelWeek from '../../Base/Label/Week.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
|
||||
export default {
|
||||
name: "MonthView",
|
||||
components: {
|
||||
CalendarGrid,
|
||||
LabelWeek,
|
||||
LabelDow,
|
||||
LabelDay
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
// NOTE(chris): snap events to day
|
||||
events: Vue.computed(() => {
|
||||
//const events = [];
|
||||
const events = this.events.map(event => {
|
||||
const start = event.start.startOf('day');
|
||||
const end = event.end.plus({ days: 1 }).startOf('day');
|
||||
return {
|
||||
...event,
|
||||
start,
|
||||
end
|
||||
};
|
||||
});
|
||||
for (var w = 5; w > -1; w--) {
|
||||
for (var d = 6; d > -1; d--) {
|
||||
const startdate = this.axisMain[w].plus(this.axisParts[d]);
|
||||
events.unshift({
|
||||
start: startdate,
|
||||
end: startdate.plus({ days: 1 }),
|
||||
orig: 'header'
|
||||
});
|
||||
}
|
||||
}
|
||||
return events;
|
||||
})
|
||||
};
|
||||
},
|
||||
inject: {
|
||||
events: "events",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
axisMain() {
|
||||
const start = this.day.startOf('month').startOf('week', { useLocaleWeeks: true });
|
||||
return Array.from({ length: 6 }, (e, i) => start.plus({ weeks: i }));
|
||||
},
|
||||
axisParts() {
|
||||
return Array.from({ length: 8 }, (e, i) => luxon.Duration.fromObject({ days: i }));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isToday(date) {
|
||||
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-mode-month-view h-100">
|
||||
<calendar-grid
|
||||
flip-axis
|
||||
:axis-main="axisMain"
|
||||
:axis-parts="axisParts"
|
||||
|
||||
snap-to-grid
|
||||
>
|
||||
<template #main-header="{ date }">
|
||||
<label-week v-bind="{ date }" />
|
||||
</template>
|
||||
<template #part-header="{ part }">
|
||||
<label-dow :date="axisMain[0].plus(part)" class="text-center" />
|
||||
</template>
|
||||
<template #event="slot">
|
||||
<label-day
|
||||
v-if="slot.event.orig == 'header'"
|
||||
:date="slot.event.start"
|
||||
:class="{ disabled: !day.hasSame(slot.event.start, 'month'), today: isToday(slot.event.start) }"
|
||||
/>
|
||||
<div v-else-if="slot.event.type == 'loading'" class="placeholder-glow opacity-50">
|
||||
<span class="placeholder w-100 fs-1" />
|
||||
</div>
|
||||
<slot v-else v-bind="slot" />
|
||||
</template>
|
||||
</calendar-grid>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import WeekView from './Week/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeWeek",
|
||||
components: {
|
||||
BaseSlider,
|
||||
WeekView
|
||||
},
|
||||
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.$refs.view.$refs.grid.disableAutoScroll();
|
||||
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.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.prevPage().then(this.updatePage);
|
||||
},
|
||||
nextPage() {
|
||||
this.rangeOffset = this.$refs.slider.target + 1;
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
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);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
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);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="fhc-calendar-mode-week flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<week-view ref="view" v-bind="viewAttrs(slot.offset)">
|
||||
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
|
||||
</week-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import CalendarGrid from '../../Base/Grid.js';
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
import LabelTime from '../../Base/Label/Time.js';
|
||||
|
||||
export default {
|
||||
name: "WeekView",
|
||||
components: {
|
||||
CalendarGrid,
|
||||
LabelDay,
|
||||
LabelDow,
|
||||
LabelTime
|
||||
},
|
||||
inject: {
|
||||
timeGrid: "timeGrid",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
collapseEmptyDays: Boolean
|
||||
},
|
||||
computed: {
|
||||
start() {
|
||||
return this.day.startOf('week', { useLocaleWeeks: true });
|
||||
},
|
||||
axisMain() {
|
||||
return Array.from({ length: 7 }, (e, i) => this.start.plus({ days: i }));
|
||||
},
|
||||
axisParts() {
|
||||
if (this.timeGrid) {
|
||||
// create {start, end} array
|
||||
return this.timeGrid.map(tu => {
|
||||
return {
|
||||
start: luxon.Duration.fromISOTime(tu.start),
|
||||
end: luxon.Duration.fromISOTime(tu.end)
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// create 07:00-23:00
|
||||
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isToday(date) {
|
||||
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-mode-week-view h-100">
|
||||
<calendar-grid
|
||||
ref="grid"
|
||||
:axis-main="axisMain"
|
||||
:axis-parts="axisParts"
|
||||
:axis-main-collapsible="collapseEmptyDays"
|
||||
:snap-to-grid="!!timeGrid"
|
||||
all-day-events
|
||||
>
|
||||
<template #main-header="{ date }">
|
||||
<div :class="{ today: isToday(date) }">
|
||||
<label-dow
|
||||
v-bind="{ date }"
|
||||
@cal-click="evt => evt.detail.source = 'day'"
|
||||
/>
|
||||
<label-day
|
||||
v-bind="{ date }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #part-header="{ part }">
|
||||
<label-time v-bind="{ part }" />
|
||||
</template>
|
||||
<template #event="slot">
|
||||
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
|
||||
<span class="placeholder w-100 h-100" />
|
||||
</div>
|
||||
<slot v-else v-bind="slot" />
|
||||
</template>
|
||||
</calendar-grid>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,353 +1,246 @@
|
||||
import FhcCalendar from "../../Calendar/Calendar.js";
|
||||
import CalendarDate from "../../../composables/CalendarDate.js";
|
||||
import LvModal from "../Mylv/LvModal.js";
|
||||
import LvMenu from "../Mylv/LvMenu.js"
|
||||
import FhcCalendar from "../../Calendar/Base.js";
|
||||
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
import { useEventLoader } from '../../../composables/EventLoader.js';
|
||||
|
||||
import ModeDay from '../../Calendar/Mode/Day.js';
|
||||
import ModeWeek from '../../Calendar/Mode/Week.js';
|
||||
import ModeMonth from '../../Calendar/Mode/Month.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
|
||||
const LvPlan = {
|
||||
export default {
|
||||
name: 'LvPlan',
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
inject: [
|
||||
"renderers"
|
||||
],
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
|
||||
return {
|
||||
events: null,
|
||||
modes: {
|
||||
day: Vue.markRaw(ModeDay),
|
||||
week: Vue.markRaw(ModeWeek),
|
||||
month: Vue.markRaw(ModeMonth)
|
||||
},
|
||||
modeOptions: {
|
||||
day: {
|
||||
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
|
||||
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
|
||||
compact: false
|
||||
},
|
||||
week: {
|
||||
collapseEmptyDays: false
|
||||
}
|
||||
},
|
||||
currentDay: this.propsViewData?.focus_date,
|
||||
calendarMode: this.propsViewData?.mode ?? DEFAULT_MODE_LVPLAN,
|
||||
calendarDate: new CalendarDate(new Date()),
|
||||
eventCalendarDate: new CalendarDate(new Date()),
|
||||
currentlySelectedEvent: null,
|
||||
currentDay: this.propsViewData?.focus_date ? new Date(this.propsViewData.focus_date) : new Date(),
|
||||
lv: null,
|
||||
minimized: false,
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
isModalContentResolved: false,
|
||||
isModalTitleResolved: false,
|
||||
isShowModal: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object,
|
||||
rowMinHeight: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
eventMaxHeight: {
|
||||
type: String,
|
||||
default: '125px'
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
rowMinHeight: this.rowMinHeight,
|
||||
eventMaxHeight: this.eventMaxHeight
|
||||
}
|
||||
},
|
||||
inject:["renderers"],
|
||||
watch: {
|
||||
/* events:{
|
||||
handler: function(newValue){
|
||||
if(newValue == null)
|
||||
setTimeout(()=>{
|
||||
if(this.events == null){
|
||||
this.loadEvents();
|
||||
}
|
||||
},500);
|
||||
},
|
||||
immediate: true,
|
||||
}, */
|
||||
modalLoaded:{
|
||||
handler: function (newValue) {
|
||||
if (this.isShowModal && newValue.isModalContentResolved && newValue.isModalTitleResolved) {
|
||||
this.$nextTick(() => {
|
||||
if(this.$refs.lvmodal) this.$refs.lvmodal.show();
|
||||
this.isShowModal = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
weekFirstDay: {
|
||||
handler: async function (newValue) {
|
||||
let data = await this.fetchStudiensemesterDetails(newValue);
|
||||
let { studiensemester_kurzbz, start, ende } = data.data;
|
||||
this.studiensemester_kurzbz = studiensemester_kurzbz;
|
||||
this.studiensemester_start = start;
|
||||
this.studiensemester_ende = ende;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
// forward/backward on history entries happening in lvplan
|
||||
'propsViewData.lv_id'(newVal) {
|
||||
// relevant if lv_id can be changed from within this component
|
||||
},
|
||||
'propsViewData.mode'(newVal) {
|
||||
if(this.$refs.calendar) this.$refs.calendar.setMode(newVal)
|
||||
},
|
||||
'propsViewData.focus_date'(newVal) {
|
||||
this.currentDate = new Date(newVal)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FhcCalendar, LvModal, LvMenu
|
||||
uid: null
|
||||
};
|
||||
},
|
||||
computed:{
|
||||
modalLoaded: function(){
|
||||
return { isModalContentResolved: this.isModalContentResolved, isModalTitleResolved:this.isModalTitleResolved};
|
||||
},
|
||||
|
||||
downloadLinks: function(){
|
||||
if(!this.studiensemester_start || !this.studiensemester_ende || !this.uid )return;
|
||||
let start = new Date(this.studiensemester_start);
|
||||
start = Math.floor(start.getTime()/1000);
|
||||
let ende = new Date(this.studiensemester_ende);
|
||||
ende = Math.floor(ende.getTime() / 1000);
|
||||
backgrounds() {
|
||||
let now = luxon.DateTime.now().setZone(this.viewData.timezone);
|
||||
|
||||
if (this.calendarMode == 'Month')
|
||||
return [
|
||||
{
|
||||
class: 'background-past',
|
||||
end: now.startOf('day')
|
||||
}
|
||||
];
|
||||
|
||||
let download_link =
|
||||
(format, version = "", target = "") =>
|
||||
`${FHC_JS_DATA_STORAGE_OBJECT.app_root}cis/private/lvplan/stpl_kalender.php?type=student&pers_uid=
|
||||
${this.uid}&begin=${start}&ende=${ende}&format=${format}
|
||||
${version ? '&version=' + version : ''}${target ? '&target=' + target : ''}`;
|
||||
return [
|
||||
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link('excel') },
|
||||
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link('csv') },
|
||||
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link('ical', '1', 'ical') },
|
||||
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link('ical', '2', 'ical') }
|
||||
{
|
||||
class: 'background-past',
|
||||
end: now,
|
||||
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
|
||||
}
|
||||
];
|
||||
},
|
||||
weekFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
|
||||
},
|
||||
weekLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
|
||||
},
|
||||
monthFirstDay: function () {
|
||||
return this.calendarDateToString(this.eventCalendarDate.cdFirstDayOfCalendarMonth);
|
||||
},
|
||||
monthLastDay: function () {
|
||||
return this.calendarDateToString(this.eventCalendarDate.cdLastDayOfCalendarMonth);
|
||||
},
|
||||
},
|
||||
methods:{
|
||||
modalTitleResolved: function () {
|
||||
this.isModalTitleResolved = true;
|
||||
downloadLinks() {
|
||||
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
|
||||
return false;
|
||||
|
||||
},
|
||||
modalContentResolved: function () {
|
||||
this.isModalContentResolved = true;
|
||||
|
||||
},
|
||||
// component renderers fetches from different addons
|
||||
modalTitleComponent(type){
|
||||
return this.renderers[type]?.modalTitle;
|
||||
},
|
||||
modalContentComponent(type) {
|
||||
return this.renderers[type]?.modalContent;
|
||||
},
|
||||
calendarEventComponent(type){
|
||||
return this.renderers[type]?.calendarEvent;
|
||||
},
|
||||
|
||||
const opts = { zone: this.viewData.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
const ende = luxon.DateTime
|
||||
.fromISO(this.studiensemester_ende, opts)
|
||||
.toUnixInteger();
|
||||
|
||||
fetchStudiensemesterDetails: async function (date) {
|
||||
return this.$api.call(ApiLvPlan.studiensemesterDateInterval(date));
|
||||
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/lvplan/stpl_kalender.php'
|
||||
+ '?type=student'
|
||||
+ '&pers_uid=' + this.uid
|
||||
+ '&begin=' + start
|
||||
+ '&ende=' + ende;
|
||||
|
||||
return [
|
||||
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
|
||||
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
|
||||
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
|
||||
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
eventStyle(event) {
|
||||
if (!event.farbe)
|
||||
return undefined;
|
||||
return '--event-bg:#' + event.farbe;
|
||||
},
|
||||
|
||||
setSelectedEvent: function (event) {
|
||||
this.currentlySelectedEvent = event;
|
||||
},
|
||||
selectDay: function(day){
|
||||
const date = day.getFullYear() + "-" +
|
||||
String(day.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(day.getDate()).padStart(2, "0");
|
||||
const capitalizedMode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
|
||||
handleChangeDate(day) {
|
||||
const focus_date = day.toISODate();
|
||||
const mode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
|
||||
|
||||
this.$router.push({
|
||||
name: "LvPlan",
|
||||
params: {
|
||||
mode: capitalizedMode,
|
||||
focus_date: date,
|
||||
mode,
|
||||
focus_date,
|
||||
lv_id: this.propsViewData?.lv_id || null
|
||||
}
|
||||
})
|
||||
|
||||
this.currentDay = day;
|
||||
},
|
||||
handleChangeMode(mode) {
|
||||
let m = mode[0].toUpperCase() + mode.slice(1)
|
||||
if(m === this.calendarMode) return; // TODO(chris): check for date and lv_id too!
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
if (m == 'Weeks' || m == 'Years' || m == 'Months') return;
|
||||
handleChangeMode(newMode) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = (this.currentDay instanceof luxon.DateTime)
|
||||
? this.currentDay.toISODate()
|
||||
: this.currentDay;
|
||||
|
||||
this.$router.push({
|
||||
name: "LvPlan",
|
||||
params: {
|
||||
mode: m,
|
||||
focus_date: date,
|
||||
mode,
|
||||
focus_date,
|
||||
lv_id: this.propsViewData?.lv_id ?? null
|
||||
}
|
||||
})
|
||||
this.calendarMode = m
|
||||
},
|
||||
showModal: function(event){
|
||||
this.currentlySelectedEvent = event;
|
||||
Vue.nextTick(()=>{
|
||||
if(this.isModalContentResolved && this.isModalTitleResolved){
|
||||
if(this.$refs.lvmodal) this.$refs.lvmodal.show();
|
||||
}
|
||||
else{
|
||||
this.isShowModal = true;
|
||||
}
|
||||
})
|
||||
},
|
||||
updateRange: function ({start,end, mounted}) {
|
||||
let checkDate = (date) => {
|
||||
return date.m != this.eventCalendarDate.m || date.y != this.eventCalendarDate.y;
|
||||
}
|
||||
this.calendarDate = new CalendarDate(end);
|
||||
|
||||
// only load month data if the month or year has changed
|
||||
// or we receive a reload flag from the mounted routine of the components
|
||||
// or this handler is being called from the mounted lifecycle of a component
|
||||
if (mounted || (checkDate(new CalendarDate(start)) && checkDate(new CalendarDate(end)))){
|
||||
// reset the events before querying the new events to activate the loading spinner
|
||||
this.events = null;
|
||||
this.eventCalendarDate = new CalendarDate(end);
|
||||
this.loadEvents();
|
||||
}
|
||||
},
|
||||
calendarDateToString: function (calendarDate) {
|
||||
return calendarDate instanceof CalendarDate ?
|
||||
[calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
|
||||
null;
|
||||
|
||||
},
|
||||
loadEvents: function(){
|
||||
Promise.allSettled([
|
||||
this.$api.call(ApiLvPlan.LvPlanEvents(this.monthFirstDay, this.monthLastDay, this.propsViewData.lv_id)),
|
||||
this.$api.call(ApiLvPlan.getLvPlanReservierungen(this.monthFirstDay, this.monthLastDay))
|
||||
]).then((result) => {
|
||||
let promise_events = [];
|
||||
result.forEach((promise_result) => {
|
||||
if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") {
|
||||
|
||||
if(promise_result.value.meta?.lv) this.lv = promise_result.value.meta.lv
|
||||
|
||||
let data = promise_result.value.data;
|
||||
// adding additional information to the events
|
||||
if (data && data.forEach) {
|
||||
|
||||
data.forEach((el, i) => {
|
||||
el.id = i;
|
||||
if (el.type === 'reservierung') {
|
||||
el.color = '#' + (el.farbe || 'FFFFFF');
|
||||
} else {
|
||||
el.color = '#' + (el.farbe || 'CCCCCC');
|
||||
}
|
||||
|
||||
el.start = new Date(el.datum + ' ' + el.beginn);
|
||||
el.end = new Date(el.datum + ' ' + el.ende);
|
||||
|
||||
});
|
||||
}
|
||||
promise_events = promise_events.concat(data);
|
||||
}
|
||||
})
|
||||
this.events = promise_events;
|
||||
});
|
||||
|
||||
this.calendarMode = mode;
|
||||
},
|
||||
updateRange(rangeInterval) {
|
||||
this.rangeInterval = rangeInterval;
|
||||
this.$api
|
||||
.call(ApiLvPlan.studiensemesterDateInterval(
|
||||
this.rangeInterval.end.startOf('week').toISODate()
|
||||
))
|
||||
.then(res => {
|
||||
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
|
||||
this.studiensemester_start = res.data.start;
|
||||
this.studiensemester_ende = res.data.ende;
|
||||
});
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const $api = Vue.inject('$api');
|
||||
|
||||
const rangeInterval = Vue.ref(null);
|
||||
|
||||
const { events, lv } = useEventLoader(rangeInterval, (start, end) => {
|
||||
return [
|
||||
$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), props.propsViewData.lv_id)),
|
||||
$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
rangeInterval,
|
||||
events,
|
||||
lv
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
||||
|
||||
this.$api
|
||||
.call(ApiAuthinfo.getAuthUID())
|
||||
.then(res => res.data)
|
||||
.then(data => {
|
||||
this.uid = data.uid;
|
||||
.then(res => {
|
||||
this.uid = res.data.uid;
|
||||
});
|
||||
|
||||
// this.loadEvents();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
|
||||
},
|
||||
template:/*html*/`
|
||||
<template v-if="renderers">
|
||||
<h2>
|
||||
{{$p.t('lehre/stundenplan')}}
|
||||
<span style="padding-left: 0.4em;" v-show="studiensemester_kurzbz">{{studiensemester_kurzbz}}</span>
|
||||
<span style="padding-left: 0.5em;" v-show="propsViewData?.lv_id && lv"> {{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english}}</span>
|
||||
</h2>
|
||||
<hr>
|
||||
<lv-modal v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ref="lvmodal" >
|
||||
<template #modalTitle>
|
||||
<Suspense @pending="isModalTitleResolved=false" @resolve="modalTitleResolved">
|
||||
<component :is="modalTitleComponent(currentlySelectedEvent.type)" v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
<template #modalContent>
|
||||
<Suspense @pending="isModalContentResolved=false" @resolve="modalContentResolved">
|
||||
<component :is="modalContentComponent(currentlySelectedEvent.type)" v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</lv-modal>
|
||||
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
@selectedEvent="setSelectedEvent"
|
||||
:initial-date="currentDay"
|
||||
@change:range="updateRange"
|
||||
@change:offset="handleOffset"
|
||||
:events="events"
|
||||
:initial-mode="propsViewData.mode"
|
||||
show-weeks
|
||||
@select:day="selectDay"
|
||||
@change:mode="handleChangeMode"
|
||||
v-model:minimized="minimized"
|
||||
>
|
||||
<template #calendarDownloads>
|
||||
<div v-for="{title,icon,link} in downloadLinks">
|
||||
<a :href="link" :aria-label="title" class="py-1 px-2 m-1 btn btn-outline-secondary card">
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span class="small">{{title}}</span>
|
||||
<div class="fhc-lvplan d-flex flex-column h-100" v-if="renderers">
|
||||
<h2 @click="modeOptions.week.collapseEmptyDays = !modeOptions.week.collapseEmptyDays">
|
||||
{{ $p.t('lehre/stundenplan') }}
|
||||
<span style="padding-left: 0.4em;" v-show="studiensemester_kurzbz">
|
||||
{{ studiensemester_kurzbz }}
|
||||
</span>
|
||||
<span style="padding-left: 0.5em;" v-show="propsViewData?.lv_id && lv">
|
||||
{{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english }}
|
||||
</span>
|
||||
</h2>
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
:date="currentDay"
|
||||
:modes="modes"
|
||||
:mode-options="modeOptions"
|
||||
:mode="propsViewData.mode.toLowerCase()"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
:timezone="viewData.timezone"
|
||||
:locale="$p.user_locale.value"
|
||||
show-btns
|
||||
:events="events || []"
|
||||
:backgrounds="backgrounds"
|
||||
>
|
||||
<template v-slot="{ event, mode }">
|
||||
<div
|
||||
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
|
||||
:type="mode == 'day' ? 'button' : undefined"
|
||||
:style="eventStyle(event)"
|
||||
>
|
||||
<component
|
||||
v-if="mode == 'event'"
|
||||
:is="renderers[event.type]?.modalContent"
|
||||
:event="event"
|
||||
></component>
|
||||
<component
|
||||
v-else-if="mode == 'eventheader'"
|
||||
:is="renderers[event.type]?.modalTitle"
|
||||
:event="event"
|
||||
></component>
|
||||
<component
|
||||
v-else
|
||||
:is="renderers[event.type]?.calendarEvent"
|
||||
:event="event"
|
||||
></component>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div
|
||||
v-if="downloadLinks"
|
||||
class="d-flex gap-1 justify-items-start"
|
||||
>
|
||||
<div v-for="{ title, icon, link } in downloadLinks">
|
||||
<a
|
||||
:href="link"
|
||||
:aria-label="title"
|
||||
class="py-1 px-2 m-1 btn btn-outline-secondary card"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span class="small">{{ title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #monthPage="{event,day}">
|
||||
<div @click="showModal(event)" class="monthPageContainer " >
|
||||
<component :is="calendarEventComponent(event.type)" :event="event" ></component>
|
||||
</div>
|
||||
</template>
|
||||
<template #weekPage="{event,day}">
|
||||
<div @click="showModal(event)" type = "button"
|
||||
class="weekPageContainer position-relative h-100"
|
||||
:class="{'p-1':event.allDayEvent}"
|
||||
style = "overflow: auto;" >
|
||||
<component :is="calendarEventComponent(event.type)" :event="event" ></component>
|
||||
</div>
|
||||
</template>
|
||||
<template #dayPage="{event,day,mobile}">
|
||||
<div @click="mobile? showModal(event):null" type="button" class="dayPageContainer fhc-entry m-0 h-100 text-center">
|
||||
<component :is="calendarEventComponent(event.type)" :event="event"></component>
|
||||
</div>
|
||||
</template>
|
||||
<template #pageMobilContent="{lvMenu, event}">
|
||||
<component :is="modalContentComponent(currentlySelectedEvent.type)" v-if="event" :event="event" :lvMenu="lvMenu" ></component>
|
||||
</template>
|
||||
<template #pageMobilContentEmpty >
|
||||
<h3>{{ $p.t('lehre/noLvFound') }}</h3>
|
||||
</template>
|
||||
</fhc-calendar>
|
||||
</template>`
|
||||
}
|
||||
|
||||
export default LvPlan;
|
||||
</div>
|
||||
</template>
|
||||
</fhc-calendar>
|
||||
</div>`
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
class="menu-entry p-2 w-100 text-wrap border border-1 rounded-3 d-flex flex-column align-items-center justify-content-center text-center text-decoration-none link h-100">
|
||||
<img :src="menuItem.c4_icon" :alt="menuItem.name" />
|
||||
<p class="w-100 mt-2 mb-0">{{menuItem.name}}</p>
|
||||
<a v-for="([text,link],index) in menuItem.c4_linkList" target="_blank" :href="link" class="my-1 w-100 submenu text-decoration-none" :index="index">{{text}}</p>
|
||||
<a v-for="([text,link],index) in menuItem.c4_linkList" target="_blank" :href="link" class="my-1 w-100 submenu text-decoration-none" :index="index">{{text}}</a>
|
||||
</a>
|
||||
<ul v-if="menuItem.c4_moodle_links?.length" class="dropdown-menu p-0" :aria-labelledby="menuItem.name">
|
||||
<li v-for="item in menuItem.c4_moodle_links"><a class="dropdown-item border-bottom" :href="item.url">{{item.lehrform}}</a></li>
|
||||
|
||||
@@ -1,259 +1,204 @@
|
||||
import FhcCalendar from "../../Calendar/Calendar.js";
|
||||
import CalendarDate from "../../../composables/CalendarDate.js";
|
||||
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
|
||||
import LvInfo from "../../../components/Cis/Mylv/LvInfo.js"
|
||||
import FhcCalendar from "../../Calendar/Base.js";
|
||||
|
||||
import ApiStudenplan from '../../../api/factory/lvPlan.js';
|
||||
|
||||
import { useEventLoader } from '../../../composables/EventLoader.js';
|
||||
|
||||
import ModeDay from '../../Calendar/Mode/Day.js';
|
||||
import ModeWeek from '../../Calendar/Mode/Week.js';
|
||||
import ModeMonth from '../../Calendar/Mode/Month.js';
|
||||
|
||||
export const DEFAULT_MODE_RAUMINFO = 'Week'
|
||||
|
||||
const RoomInformation = {
|
||||
export default {
|
||||
name: "RoomInformation",
|
||||
props:{
|
||||
propsViewData: {
|
||||
type: Object
|
||||
},
|
||||
rowMinHeight: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
eventMaxHeight: {
|
||||
type: String,
|
||||
default: '125px'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FhcCalendar,
|
||||
LvModal,
|
||||
LvInfo,
|
||||
FhcCalendar
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
rowMinHeight: this.rowMinHeight,
|
||||
eventMaxHeight: this.eventMaxHeight
|
||||
}
|
||||
inject: [
|
||||
"renderers"
|
||||
],
|
||||
props:{
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
events: null,
|
||||
calendarMode: DEFAULT_MODE_RAUMINFO,
|
||||
calendarDate: new CalendarDate(new Date()),
|
||||
currentlySelectedEvent: null,
|
||||
currentDay: this.propsViewData?.focus_date ? new Date(this.propsViewData.focus_date) : new Date(),
|
||||
minimized: false,
|
||||
|
||||
}
|
||||
modes: {
|
||||
day: Vue.markRaw(ModeDay),
|
||||
week: Vue.markRaw(ModeWeek),
|
||||
month: Vue.markRaw(ModeMonth)
|
||||
},
|
||||
modeOptions: {
|
||||
day: {
|
||||
emptyMessage: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung')),
|
||||
emptyMessageDetails: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung')),
|
||||
compact: false
|
||||
},
|
||||
week: {
|
||||
collapseEmptyDays: false
|
||||
}
|
||||
},
|
||||
currentDay: this.propsViewData?.focus_date,
|
||||
calendarMode: this.propsViewData?.mode ?? DEFAULT_MODE_RAUMINFO
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
currentDate: function(){
|
||||
return new Date(this.calendarWeek.y, this.calendarWeek.m, this.calendarWeek.d);
|
||||
},
|
||||
weekFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
|
||||
},
|
||||
weekLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
|
||||
},
|
||||
monthFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfCalendarMonth);
|
||||
},
|
||||
monthLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfCalendarMonth);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'propsViewData.ort_kurzbz'(newVal) {
|
||||
// relevant if ort_kurzbz can be changed from within this component
|
||||
},
|
||||
'propsViewData.mode'(newVal) {
|
||||
if(this.$refs.calendar) this.$refs.calendar.setMode(newVal)
|
||||
},
|
||||
'propsViewData.focus_date'(newVal) {
|
||||
this.currentDate = new Date(newVal)
|
||||
}
|
||||
computed: {
|
||||
backgrounds() {
|
||||
let now = luxon.DateTime.now().setZone(this.viewData.timezone);
|
||||
|
||||
if (this.calendarMode == 'Month')
|
||||
return [
|
||||
{
|
||||
class: 'background-past',
|
||||
end: now.startOf('day')
|
||||
}
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
class: 'background-past',
|
||||
end: now,
|
||||
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
setSelectedEvent: function(event){
|
||||
this.currentlySelectedEvent = event;
|
||||
methods:{
|
||||
eventStyle(event) {
|
||||
if (!event.farbe)
|
||||
return undefined;
|
||||
return '--event-bg:#' + event.farbe;
|
||||
},
|
||||
getLvID: function () {
|
||||
this.lv_id = window.location.pathname
|
||||
},
|
||||
selectDay: function(day){
|
||||
const date = day.getFullYear() + "-" +
|
||||
String(day.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(day.getDate()).padStart(2, "0");
|
||||
handleChangeDate(day) {
|
||||
const focus_date = day.toISODate();
|
||||
const mode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
|
||||
|
||||
this.$router.push({
|
||||
name: "RoomInformation",
|
||||
params: {
|
||||
mode: this.calendarMode,
|
||||
focus_date: date,
|
||||
mode,
|
||||
focus_date,
|
||||
ort_kurzbz: this.propsViewData.ort_kurzbz
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
this.currentDay = day;
|
||||
},
|
||||
handleOffset: function(offset) {
|
||||
this.currentDay = new Date(
|
||||
this.currentDay.getFullYear() + offset.y,
|
||||
this.currentDay.getMonth() + offset.m,
|
||||
this.currentDay.getDate() + offset.d
|
||||
)
|
||||
handleChangeMode(newMode) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = (this.currentDay instanceof luxon.DateTime)
|
||||
? this.currentDay.toISODate()
|
||||
: this.currentDay;
|
||||
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "LvPlan",
|
||||
params: {
|
||||
mode: this.calendarMode,
|
||||
focus_date: date,
|
||||
lv_id: this.propsViewData?.lv_id || null
|
||||
}
|
||||
})
|
||||
},
|
||||
handleChangeMode(mode) {
|
||||
const modeCapitalized = mode.charAt(0).toUpperCase() + mode.slice(1)
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "RoomInformation",
|
||||
params: {
|
||||
mode: modeCapitalized,
|
||||
focus_date: date,
|
||||
mode,
|
||||
focus_date,
|
||||
ort_kurzbz: this.propsViewData.ort_kurzbz
|
||||
}
|
||||
})
|
||||
|
||||
this.calendarMode = mode
|
||||
},
|
||||
showModal: function (event) {
|
||||
this.currentlySelectedEvent = event;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.lvmodal.show();
|
||||
});
|
||||
|
||||
},
|
||||
updateRange: function ({ start, end }) {
|
||||
|
||||
let checkDate = (date) => {
|
||||
return date.m != this.calendarDate.m || date.y != this.calendarDate.y;
|
||||
}
|
||||
|
||||
// only load month data if the month or year has changed
|
||||
if (checkDate(new CalendarDate(start)) && checkDate(new CalendarDate(end))) {
|
||||
// reset the events before querying the new events to activate the loading spinner
|
||||
this.events = null;
|
||||
this.calendarDate = new CalendarDate(end);
|
||||
Vue.nextTick(() => {
|
||||
this.loadEvents();
|
||||
});
|
||||
}
|
||||
},
|
||||
calendarDateToString: function (calendarDate) {
|
||||
return calendarDate instanceof CalendarDate ?
|
||||
[calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
|
||||
null;
|
||||
|
||||
},
|
||||
loadEvents: function(){
|
||||
|
||||
// bundles the room_events and the reservierungen together into the this.events array
|
||||
Promise.allSettled([
|
||||
this.$api.call(ApiStudenplan.getRoomInfo(this.propsViewData.ort_kurzbz, this.monthFirstDay, this.monthLastDay)),
|
||||
this.$api.call(ApiStudenplan.getOrtReservierungen(this.propsViewData.ort_kurzbz, this.monthFirstDay, this.monthLastDay))
|
||||
]).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;
|
||||
// adding additional information to the events
|
||||
if (data && data.forEach) {
|
||||
data.forEach((el, i) => {
|
||||
el.id = i;
|
||||
if (el.type === 'reservierung') {
|
||||
el.color = '#' + (el.farbe || 'FFFFFF');
|
||||
} else {
|
||||
el.color = '#' + (el.farbe || 'CCCCCC');
|
||||
}
|
||||
|
||||
el.start = new Date(el.datum + ' ' + el.beginn);
|
||||
el.end = new Date(el.datum + ' ' + el.ende);
|
||||
|
||||
});
|
||||
}
|
||||
promise_events = promise_events.concat(data);
|
||||
}
|
||||
})
|
||||
this.events = promise_events;
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadEvents();
|
||||
updateRange(rangeInterval) {
|
||||
this.rangeInterval = rangeInterval;
|
||||
}
|
||||
},
|
||||
template: /*html*/`
|
||||
setup(props) {
|
||||
const $api = Vue.inject('$api');
|
||||
|
||||
const rangeInterval = Vue.ref(null);
|
||||
|
||||
const { events } = useEventLoader(rangeInterval, (start, end) => {
|
||||
return [
|
||||
$api.call(ApiStudenplan.getRoomInfo(props.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate())),
|
||||
$api.call(ApiStudenplan.getOrtReservierungen(props.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate()))
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
rangeInterval,
|
||||
events
|
||||
};
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="fhc-roominformation d-flex flex-column h-100">
|
||||
<h2>{{ $p.t('rauminfo/rauminfo') }} {{ propsViewData.ort_kurzbz }}</h2>
|
||||
<hr>
|
||||
<lv-modal v-if="currentlySelectedEvent" :showMenu="false" :event="currentlySelectedEvent" ref="lvmodal" />
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
@selectedEvent="setSelectedEvent"
|
||||
:initial-date="currentDay"
|
||||
@change:range="updateRange"
|
||||
@change:offset="handleOffset"
|
||||
:events="events"
|
||||
:initial-mode="propsViewData.mode"
|
||||
show-weeks
|
||||
@select:day="selectDay"
|
||||
@change:mode="handleChangeMode"
|
||||
v-model:minimized="minimized"
|
||||
:date="currentDay"
|
||||
:modes="modes"
|
||||
:mode-options="modeOptions"
|
||||
:mode="propsViewData.mode.toLowerCase()"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
:timezone="viewData.timezone"
|
||||
:locale="$p.user_locale.value"
|
||||
show-btns
|
||||
:events="events || []"
|
||||
:backgrounds="backgrounds"
|
||||
>
|
||||
<template #monthPage="{event,day}">
|
||||
<span >
|
||||
{{event.topic}}
|
||||
</span>
|
||||
</template>
|
||||
<template #weekPage="{event,day}">
|
||||
<div @click="showModal(event)" type="button" class=" border border-secondary border d-flex flex-column align-items-center justify-content-evenly h-100">
|
||||
<span>{{event?.topic}}</span>
|
||||
<span v-for="lektor in event?.lektor">{{lektor.kurzbz}}</span>
|
||||
<span>{{event?.ort_kurzbz}}</span>
|
||||
<template v-slot="{ event, mode }">
|
||||
<component
|
||||
v-if="mode == 'event'"
|
||||
:is="renderers[event.type]?.modalContent"
|
||||
:event="event"
|
||||
></component>
|
||||
<component
|
||||
v-else-if="mode == 'eventheader'"
|
||||
:is="renderers[event.type]?.modalTitle"
|
||||
:event="event"
|
||||
></component>
|
||||
<div
|
||||
v-else-if="mode == 'month'"
|
||||
:class="'event-type-' + event.type"
|
||||
:style="eventStyle(event)"
|
||||
class="d-flex flex-column align-items-center justify-content-evenly h-100"
|
||||
>
|
||||
<span>{{ event?.topic }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #dayPage="{event,day,mobile}">
|
||||
<div @click="mobile? showModal(event):null" type="button" class="fhc-entry border border-secondary border row h-100 justify-content-center align-items-center text-center">
|
||||
<div
|
||||
v-else-if="mode == 'week'"
|
||||
:class="'event-type-' + event.type"
|
||||
:style="eventStyle(event)"
|
||||
class="border border-secondary d-flex flex-column align-items-center justify-content-evenly h-100"
|
||||
type="button"
|
||||
>
|
||||
<span>{{ event?.topic }}</span>
|
||||
<span v-for="lektor in event?.lektor">{{ lektor.kurzbz }}</span>
|
||||
<span>{{ event?.ort_kurzbz }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="mode == 'day'"
|
||||
:class="'event-type-' + event.type"
|
||||
:style="eventStyle(event)"
|
||||
type="button"
|
||||
class="border border-secondary d-flex align-items-center justify-content-center text-center h-100"
|
||||
>
|
||||
<div class="col ">
|
||||
<p>{{ $p.t('lehre/lehrveranstaltung') }}:</p>
|
||||
<p class="m-0">{{event?.topic}}</p>
|
||||
<p class="m-0">
|
||||
{{ event?.topic }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col ">
|
||||
<p>{{ $p.t('lehre/lektor') }}:</p>
|
||||
<p class="m-0" v-for="lektor in event?.lektor">{{lektor.kurzbz}}</p>
|
||||
<p class="m-0" v-for="lektor in event?.lektor">
|
||||
{{ lektor.kurzbz }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col ">
|
||||
<p>{{ $p.t('profil/Ort') }}: </p>
|
||||
<p class="m-0">{{event?.ort_kurzbz}}</p>
|
||||
<p class="m-0">
|
||||
{{ event?.ort_kurzbz }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #pageMobilContent>
|
||||
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
|
||||
<div class="w-100">
|
||||
<lv-info :event="currentlySelectedEvent" />
|
||||
</div>
|
||||
</template>
|
||||
<template #pageMobilContentEmpty >
|
||||
<h3>{{$p.t('rauminfo','keineRaumReservierung')}}</h3>
|
||||
</template>
|
||||
</fhc-calendar>
|
||||
`,
|
||||
</fhc-calendar>
|
||||
</div>`
|
||||
};
|
||||
|
||||
export default RoomInformation
|
||||
@@ -1,20 +1,23 @@
|
||||
import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
|
||||
import LvMenu from "../../Mylv/LvMenu.js";
|
||||
|
||||
import ApiLvPlan from '../../../../api/factory/lvPlan.js';
|
||||
import ApiAddons from '../../../../api/factory/addons.js';
|
||||
|
||||
export default {
|
||||
components:{
|
||||
LvMenu,
|
||||
},
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
lvMenu:{
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
components:{
|
||||
LvMenu,
|
||||
data() {
|
||||
return {
|
||||
lvMenu: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lektorenLinks: function () {
|
||||
@@ -33,14 +36,14 @@ export default {
|
||||
},
|
||||
start_time: function () {
|
||||
if (!this.event.start) return 'N/A';
|
||||
if (!this.event.start instanceof Date) {
|
||||
if (!(this.event.start instanceof Date)) {
|
||||
return this.event.start;
|
||||
}
|
||||
return numberPadding(this.event.start.getHours()) + ":" + numberPadding(this.event.start.getMinutes());
|
||||
},
|
||||
end_time: function () {
|
||||
if (!this.event.end) return 'N/A';
|
||||
if (!this.event.end instanceof Date) {
|
||||
if (!(this.event.end instanceof Date)) {
|
||||
return this.event.end;
|
||||
}
|
||||
return numberPadding(this.event.end.getHours()) + ":" + numberPadding(this.event.end.getMinutes());
|
||||
@@ -54,6 +57,22 @@ export default {
|
||||
return formatDate(d);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.event.type == 'lehreinheit') {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getLehreinheitStudiensemester(Array.isArray(this.event.lehreinheit_id) ? this.event.lehreinheit_id[0] : this.event.lehreinheit_id))
|
||||
.then(res => res.data)
|
||||
.then(studiensemester_kurzbz => this.$api.call(
|
||||
ApiAddons.getLvMenu(
|
||||
this.event.lehrveranstaltung_id,
|
||||
studiensemester_kurzbz
|
||||
)
|
||||
))
|
||||
.then(res => {
|
||||
this.lvMenu = res.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h5>
|
||||
@@ -122,6 +141,6 @@ export default {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-if="lvMenu" :menu="lvMenu" />
|
||||
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-if="lvMenu.length" :menu="lvMenu" />
|
||||
</div>`,
|
||||
}
|
||||
@@ -19,9 +19,8 @@ export default {
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({name: '', uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.name && value.uid
|
||||
return value && value.name && value.uid && value.timezone
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -37,6 +36,7 @@ export default {
|
||||
return {
|
||||
editMode: Vue.computed(()=>this.editMode),
|
||||
widgetsSetup: Vue.computed(() => this.widgets),
|
||||
timezone: Vue.computed(() => this.viewData.timezone)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -209,11 +209,11 @@ export default {
|
||||
});
|
||||
},
|
||||
template: `
|
||||
<h4 v-if="editModeIsActive" class=" mb-2">
|
||||
<i v-tooltip="showSectionInformation(name)" class="fa-solid fa-circle-info section-info" ></i>
|
||||
{{sectionNameTranslation()}}:
|
||||
</h4>
|
||||
<div class="dashboard-section position-relative pb-3 border-bottom" ref="container" :style="getSectionStyle">
|
||||
<h4 v-if="editModeIsActive" class=" mb-2">
|
||||
<i v-tooltip="showSectionInformation(name)" class="fa-solid fa-circle-info section-info" ></i>
|
||||
{{sectionNameTranslation()}}:
|
||||
</h4>
|
||||
<button v-tooltip="$p.t('dashboard','addLine')" v-if="!additionalRow && editModeIsActive" @click="additionalRow=true" class="btn btn-outline-secondary rounded-circle newGridRow d-flex justify-content-center align-items-center">+</button>
|
||||
<drop-grid v-model:cols="gridWidth" v-model:additionalRow="additionalRow" :items="items" :itemsSetup="computedWidgetsSetup" :active="editModeIsActive" :resize-limit="checkResizeLimit" :margin-for-extra-row=".01" @draggedItem="draggedItem=$event" @rearrange-items="updatePositions" @gridHeight="gridHeight=$event" >
|
||||
<template #default="item">
|
||||
|
||||
@@ -1,266 +1,115 @@
|
||||
import Phrasen from '../../mixins/Phrasen.js';
|
||||
import AbstractWidget from './Abstract.js';
|
||||
import FhcCalendar from '../Calendar/Calendar.js';
|
||||
import LvModal from '../Cis/Mylv/LvModal.js';
|
||||
import ContentModal from '../Cis/Cms/ContentModal.js'
|
||||
import CalendarDate from '../../composables/CalendarDate.js';
|
||||
import FhcCalendar from '../Calendar/Base.js';
|
||||
|
||||
import ApiLvPlan from '../../api/factory/lvPlan.js';
|
||||
import ApiOrt from '../../api/factory/ort.js';
|
||||
|
||||
import { useEventLoader } from '../../composables/EventLoader.js';
|
||||
|
||||
import ModeList from '../Calendar/Mode/List.js';
|
||||
|
||||
export default {
|
||||
name: "LvPlanWidget",
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
mixins: [
|
||||
Phrasen,
|
||||
AbstractWidget
|
||||
],
|
||||
components: {
|
||||
FhcCalendar,
|
||||
LvModal,
|
||||
ContentModal
|
||||
},
|
||||
|
||||
inject: [
|
||||
"renderers",
|
||||
"timezone"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
stunden: [],
|
||||
minimized: true,
|
||||
events: null,
|
||||
currentDay: this.getCurrentDate(),
|
||||
calendarDate: new CalendarDate(new Date()),
|
||||
roomInfoContentID: null,
|
||||
ort_kurzbz: null,
|
||||
selectedEvent: null,
|
||||
isModalContentResolved: false,
|
||||
isModalTitleResolved: false,
|
||||
isShowModal: false,
|
||||
}
|
||||
},
|
||||
inject: ["renderers"],
|
||||
watch:{
|
||||
modalLoaded: {
|
||||
handler: function (newValue) {
|
||||
if (this.isShowModal && newValue.isModalContentResolved && newValue.isModalTitleResolved) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.lvmodal) this.$refs.lvmodal.show();
|
||||
this.isShowModal = false;
|
||||
});
|
||||
now: luxon.DateTime.now().setZone(this.timezone),
|
||||
modes: {
|
||||
list: Vue.markRaw(ModeList)
|
||||
},
|
||||
modeOptions: {
|
||||
list: {
|
||||
length: 7
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalLoaded: function () {
|
||||
return { isModalContentResolved: this.isModalContentResolved, isModalTitleResolved: this.isModalTitleResolved };
|
||||
},
|
||||
allEventsGrouped() {
|
||||
// groups all events of the next 7 days together
|
||||
const currentCalendarDate = new CalendarDate(this.currentDay)
|
||||
const mapArr = currentCalendarDate.nextSevenDays.map((d) => [new CalendarDate(d), []])
|
||||
|
||||
// return map of key => calendar date of next 7 days & values the respective events on that date
|
||||
return new Map((this.events || []).filter(evt => evt.start >= this.currentDay).reduce((acc, cur) => {
|
||||
const date = new CalendarDate(new Date(cur.datum))
|
||||
const arr = acc.find(el => el[0].compare(date))
|
||||
if(!arr) return acc
|
||||
arr[1].push(cur)
|
||||
|
||||
return acc
|
||||
}, mapArr));
|
||||
},
|
||||
currentEvents() {
|
||||
return (this.events || []).filter(evt => evt.end < this.dayAfterCurrentDay && evt.start >= this.currentDay);
|
||||
},
|
||||
dayAfterCurrentDay() {
|
||||
let currentDay = new Date(this.currentDay);
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
return currentDay;
|
||||
},
|
||||
monthFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfCalendarMonth);
|
||||
},
|
||||
monthLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfNextCalendarMonth);
|
||||
},
|
||||
currentDay: luxon.DateTime.now().setZone(this.timezone).startOf('day')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showModal: function (event) {
|
||||
this.currentlySelectedEvent = event;
|
||||
Vue.nextTick(() => {
|
||||
if (this.isModalContentResolved && this.isModalTitleResolved) {
|
||||
if (this.$refs.lvmodal) this.$refs.lvmodal.show();
|
||||
}
|
||||
else {
|
||||
this.isShowModal = true;
|
||||
}
|
||||
})
|
||||
},
|
||||
modalTitleResolved: function () {
|
||||
this.isModalTitleResolved = true;
|
||||
|
||||
},
|
||||
modalContentResolved: function () {
|
||||
this.isModalContentResolved = true;
|
||||
|
||||
},
|
||||
modalTitleComponent(type) {
|
||||
return this.renderers[type]?.modalTitle;
|
||||
},
|
||||
modalContentComponent(type) {
|
||||
return this.renderers[type]?.modalContent;
|
||||
},
|
||||
calendarEventComponent(type) {
|
||||
return this.renderers[type]?.calendarEvent;
|
||||
},
|
||||
getEventStyle: function(evt) {
|
||||
const styles = {'background-color': evt.color};
|
||||
if(evt.start.getTime() < Date.now()) styles.opacity = 0.5;
|
||||
eventStyle(event) {
|
||||
const styles = {};
|
||||
if (event.farbe)
|
||||
styles['--event-bg'] = '#' + event.farbe;
|
||||
else if (event.type == 'reservierung')
|
||||
styles['--event-bg'] = '#ffffff';
|
||||
else
|
||||
styles['--event-bg'] = '#cccccc';
|
||||
|
||||
const eventEnd = luxon.DateTime.fromISO(event.isoend, { zone: this.timezone });
|
||||
if (eventEnd < this.now)
|
||||
styles['opacity'] = .5;
|
||||
|
||||
return styles;
|
||||
},
|
||||
getCurrentDate: function() {
|
||||
const today = new Date()
|
||||
today.setHours(0,0,0)
|
||||
return today
|
||||
},
|
||||
calendarDateToString: function (calendarDate) {
|
||||
|
||||
return calendarDate instanceof CalendarDate ?
|
||||
[calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
|
||||
null;
|
||||
|
||||
},
|
||||
showRoomInfoModal: function(ort_kurzbz){
|
||||
// getting the content_id of the ort_kurzbz
|
||||
this.$api
|
||||
.call(ApiOrt.getContentID(ort_kurzbz))
|
||||
.then(res => {
|
||||
this.roomInfoContentID = res.data;
|
||||
this.ort_kurzbz = ort_kurzbz;
|
||||
|
||||
// only showing the modal after vue was able to set the reactive data
|
||||
Vue.nextTick(() => { this.$refs.contentModal.show(); });
|
||||
})
|
||||
.catch(err => {
|
||||
console.err(err);
|
||||
this.ort_kurzbz = null;
|
||||
this.roomInfoContentID = null;
|
||||
});
|
||||
},
|
||||
showLvUebersicht: function (event){
|
||||
this.selectedEvent= event;
|
||||
Vue.nextTick(()=>{
|
||||
this.$refs.lvmodal.show();
|
||||
});
|
||||
},
|
||||
|
||||
selectDay(day) {
|
||||
this.currentDay = day;
|
||||
this.minimized = true;
|
||||
},
|
||||
|
||||
updateRange: function (data) {
|
||||
|
||||
let tmp_date = new CalendarDate(data.start);
|
||||
// only load month data if the month or year has changed
|
||||
if (tmp_date.m != this.calendarDate.m || tmp_date.y != this.calendarDate.y) {
|
||||
this.calendarDate = tmp_date;
|
||||
Vue.nextTick(() => {
|
||||
this.loadEvents();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
loadEvents: function () {
|
||||
Promise.allSettled([
|
||||
this.$api.call(ApiLvPlan.LvPlanEvents(this.monthFirstDay, this.monthLastDay)),
|
||||
this.$api.call(ApiLvPlan.getLvPlanReservierungen(this.monthFirstDay, this.monthLastDay))
|
||||
]).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;
|
||||
// adding additional information to the events
|
||||
if (data && data.forEach) {
|
||||
|
||||
data.forEach((el, i) => {
|
||||
el.id = i;
|
||||
if (el.type === 'reservierung') {
|
||||
el.color = '#' + (el.farbe || 'FFFFFF');
|
||||
} else {
|
||||
el.color = '#' + (el.farbe || 'CCCCCC');
|
||||
}
|
||||
|
||||
el.start = new Date(el.datum + ' ' + el.beginn);
|
||||
el.end = new Date(el.datum + ' ' + el.ende);
|
||||
|
||||
});
|
||||
}
|
||||
promise_events = promise_events.concat(data);
|
||||
}
|
||||
})
|
||||
this.events = promise_events;
|
||||
});
|
||||
},
|
||||
|
||||
setCalendarMaximized() {
|
||||
this.minimized = false
|
||||
updateRange(rangeInterval) {
|
||||
this.rangeInterval = rangeInterval;
|
||||
}
|
||||
|
||||
},
|
||||
setup() {
|
||||
const $api = Vue.inject('$api');
|
||||
|
||||
const rangeInterval = Vue.ref(null);
|
||||
|
||||
const { events } = useEventLoader(rangeInterval, (start, end) => {
|
||||
return [
|
||||
$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate())),
|
||||
$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
rangeInterval,
|
||||
events
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$emit('setConfig', false);
|
||||
this.loadEvents();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="dashboard-widget-lvplan d-flex flex-column h-100">
|
||||
<lv-modal v-if="selectedEvent" ref="lvmodal" :event="selectedEvent" >
|
||||
<template #modalTitle>
|
||||
<Suspense @pending="isModalTitleResolved=false" @resolve="modalTitleResolved">
|
||||
<component :is="modalTitleComponent(selectedEvent.type)" v-if="selectedEvent" :event="selectedEvent" ></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
<template #modalContent>
|
||||
<Suspense @pending="isModalContentResolved=false" @resolve="modalContentResolved">
|
||||
<component :is="modalContentComponent(selectedEvent.type)" v-if="selectedEvent" :event="selectedEvent" ></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</lv-modal>
|
||||
<content-modal :content_id="roomInfoContentID" dialogClass="modal-lg" ref="contentModal"/>
|
||||
<fhc-calendar @change:range="updateRange" :initial-date="currentDay" class="border-0" class-header="p-0" @select:day="selectDay" :widget="true" v-model:minimized="minimized" :events="events" no-week-view :show-weeks="false" >
|
||||
<template #monthPage="{event,day}">
|
||||
<div v-if="event.type=='moodle'">
|
||||
<div class="d-flex small w-100" >
|
||||
<moodle-svg></moodle-svg>
|
||||
<span v-contrast class="flex-grow-1 text-center "><strong v-html="event.titel"></strong> - {{event.topic}}</span>
|
||||
</div>
|
||||
<fhc-calendar
|
||||
v-model:date="currentDay"
|
||||
:modes="modes"
|
||||
:mode-options="modeOptions"
|
||||
@update:range="updateRange"
|
||||
:timezone="timezone"
|
||||
:locale="$p.user_locale.value"
|
||||
:events="events"
|
||||
>
|
||||
<template v-slot="{ event, mode }">
|
||||
<div
|
||||
v-if="!event"
|
||||
class="h-100 d-flex justify-content-center align-items-center"
|
||||
>
|
||||
{{ $p.t('lehre/noLvFound') }}
|
||||
</div>
|
||||
<span v-else class="small" >
|
||||
{{event.topic}}
|
||||
</span>
|
||||
</template>
|
||||
<template #minimizedPage >
|
||||
<div class="minimizedContainer flex-grow-1" style="overflow-y: auto; overflow-x: hidden">
|
||||
<div v-if="events === null" class="d-flex h-100 justify-content-center align-items-center">
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<template v-else-if="allEventsGrouped.size" v-for="([key, value], index) in allEventsGrouped" :key="index" style="margin-top: 8px;">
|
||||
<div class=" card-header d-grid p-0">
|
||||
<button class="btn fhc-tertiary text-decoration-none" @click="setCalendarMaximized">{{ key.format({dateStyle: "full"}, $p.user_locale.value)}}</button>
|
||||
</div>
|
||||
<div role="button" @click="showLvUebersicht(evt)" v-for="evt in value" :key="evt.id" class="mx-1 list-group-item small" :style="getEventStyle(evt)">
|
||||
<component :is="calendarEventComponent(evt.type)" :event="evt" ></component>
|
||||
</div>
|
||||
<div v-if="!value.length" class="list-group-item small text-center">
|
||||
{{ $p.t('lehre/noLvFound') }}
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="d-flex h-100 justify-content-center align-items-center fst-italic text-center">
|
||||
{{ $p.t('lehre/noLvFound') }}
|
||||
</div>
|
||||
<component
|
||||
v-else-if="mode == 'eventheader'"
|
||||
:is="renderers[event.type]?.modalTitle"
|
||||
:event="event"
|
||||
></component>
|
||||
<component
|
||||
v-else-if="mode == 'event'"
|
||||
:is="renderers[event.type]?.modalContent"
|
||||
:event="event"
|
||||
></component>
|
||||
<div
|
||||
v-else
|
||||
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
|
||||
:style="eventStyle(event)"
|
||||
>
|
||||
<component
|
||||
:is="renderers[event.type]?.calendarEvent"
|
||||
:event="event"
|
||||
></component>
|
||||
</div>
|
||||
</template>
|
||||
</fhc-calendar>
|
||||
|
||||
@@ -36,6 +36,7 @@ export default {
|
||||
"newItem",
|
||||
"gridHeight",
|
||||
"draggedItem",
|
||||
"update:additionalRow"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// TODO(chris): load events that are longer than the interval without doubling it
|
||||
|
||||
export function useEventLoader(rangeInterval, getPromiseFunc) {
|
||||
let loading_id = 0;
|
||||
const events = Vue.ref([]);
|
||||
const loadingEvents = Vue.ref([]);
|
||||
const allEvents = Vue.computed(() => events.value.concat(loadingEvents.value));
|
||||
const lv = Vue.ref(null);
|
||||
const eventsLoaded = [];
|
||||
|
||||
const mergePromiseArr = (n, o) => {
|
||||
if (Array.isArray(n))
|
||||
return o.concat(n);
|
||||
return o.push(n), o;
|
||||
};
|
||||
|
||||
const markEventsLoaded = (start, end) => {
|
||||
let result = [];
|
||||
if (!eventsLoaded.length) {
|
||||
// empty: add new chunk
|
||||
eventsLoaded.push(start.ts, end.ts);
|
||||
} else {
|
||||
if (eventsLoaded[eventsLoaded.length-1] + 1 == start.ts) {
|
||||
// add to the end of last chunk
|
||||
eventsLoaded[eventsLoaded.length-1] = end.ts;
|
||||
} else if (eventsLoaded[eventsLoaded.length-1] < start.ts) {
|
||||
// add new chunk after the last chunk
|
||||
eventsLoaded.push(start.ts, end.ts);
|
||||
} else if (eventsLoaded[0] == end.ts + 1) {
|
||||
// add to the start of first chunk
|
||||
eventsLoaded[0] = start.ts;
|
||||
} else if (eventsLoaded[0] > end.ts) {
|
||||
eventsLoaded.unshift(start.ts, end.ts);
|
||||
} else {
|
||||
let index = eventsLoaded.findIndex(e => e >= start.ts);
|
||||
|
||||
if (index % 2) {
|
||||
// starts inside an existing chunk
|
||||
if (eventsLoaded[index] >= end.ts)
|
||||
return []; // Already loaded
|
||||
|
||||
let indexIsLast = (index == eventsLoaded.length - 1);
|
||||
|
||||
if (indexIsLast || eventsLoaded[index + 1] > end.ts) {
|
||||
// extend an existing chunk
|
||||
// and merge with the next if necessary
|
||||
let nStart = eventsLoaded[index] + 1;
|
||||
start = start.plus(nStart - start.ts);
|
||||
if (!indexIsLast && eventsLoaded[index + 1] == end.ts + 1)
|
||||
eventsLoaded.splice(index, 2);
|
||||
else
|
||||
eventsLoaded[index] = end.ts;
|
||||
} else {
|
||||
// merge exising chunks
|
||||
// and load the rest if necessary
|
||||
if (eventsLoaded[index + 2] < end.ts) {
|
||||
let rStart = eventsLoaded[index + 2] + 1;
|
||||
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
|
||||
}
|
||||
|
||||
let nStart = eventsLoaded[index] + 1;
|
||||
start = start.plus(nStart - start.ts);
|
||||
let nEnd = eventsLoaded[index + 1] - 1;
|
||||
end = end.plus(nEnd - end.ts);
|
||||
eventsLoaded.splice(index, 2);
|
||||
}
|
||||
} else {
|
||||
// starts between two chunks or before the first
|
||||
if (!index) {
|
||||
// extend the first chunk
|
||||
// and load the rest if necessary
|
||||
if (eventsLoaded[1] < end.ts) {
|
||||
let rStart = eventsLoaded[1] + 1;
|
||||
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
|
||||
}
|
||||
let nEnd = eventsLoaded[0] - 1;
|
||||
end = end.plus(nEnd - end.ts);
|
||||
eventsLoaded[0] = start.ts;
|
||||
} else if (eventsLoaded[index] == start.ts) {
|
||||
// starts at the same position as an existing chunk
|
||||
if (eventsLoaded[index + 1] >= end.ts)
|
||||
return []; // Already loaded
|
||||
// load the rest
|
||||
let rStart = eventsLoaded[index + 1] + 1;
|
||||
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
|
||||
} else {
|
||||
// extend an existing chunk
|
||||
// and load the rest if necessary
|
||||
if (eventsLoaded[index + 1] < end.ts) {
|
||||
let rStart = eventsLoaded[index + 1] + 1;
|
||||
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
|
||||
}
|
||||
let nEnd = eventsLoaded[index] - 1;
|
||||
end = end.plus(nEnd - end.ts);
|
||||
eventsLoaded[index] = start.ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (start.ts >= end.ts)
|
||||
return result;
|
||||
|
||||
loadingEvents.value.push({
|
||||
loading_id: loading_id++,
|
||||
type: "loading",
|
||||
isostart: start.toISODate() + 'T' + start.toISOTime(),
|
||||
isoend: end.toISODate() + 'T' + end.toISOTime()
|
||||
});
|
||||
|
||||
return mergePromiseArr(getPromiseFunc(start, end), result);
|
||||
};
|
||||
|
||||
Vue.watchEffect(() => {
|
||||
const range = Vue.toValue(rangeInterval);
|
||||
if (!(range instanceof luxon.Interval))
|
||||
return;
|
||||
const promises = markEventsLoaded(range.start, range.end);
|
||||
Promise
|
||||
.allSettled(promises)
|
||||
.then(results => {
|
||||
results.forEach(res => {
|
||||
if (
|
||||
res.status === 'fulfilled'
|
||||
&& res.value.meta.status === "success"
|
||||
) {
|
||||
if (res.value.meta.lv)
|
||||
lv.value = res.value.meta.lv;
|
||||
|
||||
events.value = events.value.concat(res.value.data);
|
||||
loadingEvents.value = [];
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
return { events: allEvents, lv }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
const clickListeners = [];
|
||||
|
||||
function saveAddClickListener(el, source, value) {
|
||||
const index = clickListeners.findIndex(data => data.el == el);
|
||||
if (index >= 0) {
|
||||
el.removeEventListener('click', clickListeners[index].listener);
|
||||
clickListeners.splice(index, 1);
|
||||
}
|
||||
const listener = evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const customEvent = new CustomEvent('cal-click', {
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
detail: { source, value }
|
||||
});
|
||||
evt.target.dispatchEvent(customEvent);
|
||||
}
|
||||
clickListeners.push({el, listener});
|
||||
el.addEventListener('click', listener);
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vnode) {
|
||||
if (binding.arg == 'container') {
|
||||
el.addEventListener('cal-click', evt => {
|
||||
const customEvent = new Event('click:' + evt.detail.source, {
|
||||
cancelable: true
|
||||
});
|
||||
binding.instance.$emit('click:' + evt.detail.source, customEvent, evt.detail.value);
|
||||
if (!customEvent.defaultPrevented) {
|
||||
const finalEvent = new CustomEvent('cal-click-default', {
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
detail: evt.detail
|
||||
});
|
||||
evt.target.dispatchEvent(finalEvent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveAddClickListener(el, binding.arg, binding.value);
|
||||
}
|
||||
},
|
||||
updated(el, binding, vnode, prevVnode) {
|
||||
if (binding.arg != 'container') {
|
||||
saveAddClickListener(el, binding.arg, binding.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* TODO(chris): This is only a prototype!!!
|
||||
*/
|
||||
const DragAndDrop = {
|
||||
TYPE_LE: "lehreinheit",
|
||||
TYPE_VEVENT: "vevent",
|
||||
|
||||
getValidTransferData(event, allowedTypes) {
|
||||
const json = event.dataTransfer.getData('text');
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(json);
|
||||
if (!obj.type)
|
||||
return null;
|
||||
if (allowedTypes && !allowedTypes.includes(obj.type))
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
isValidTransferData(event, allowedTypes) {
|
||||
return this.getValidTransferData(event, allowedTypes) ? true : false;
|
||||
},
|
||||
getTransferData(event) {
|
||||
const json = event.dataTransfer.getData('text');
|
||||
return JSON.parse(json);
|
||||
},
|
||||
setTransferData(event, data) {
|
||||
switch (data.type) {
|
||||
case DragAndDrop.TYPE_LE:
|
||||
data = DragAndDrop.fromLe(data);
|
||||
break;
|
||||
default:
|
||||
if (data.dtstart && data.dtend && data.uid && data.summary) {
|
||||
data = DragAndDrop.fromVEvent(data);
|
||||
break;
|
||||
}
|
||||
return false; // No type found => abort
|
||||
}
|
||||
|
||||
event.dataTransfer.setData('text', JSON.stringify(data));
|
||||
return true;
|
||||
},
|
||||
fromLe(data) {
|
||||
const {
|
||||
type = DragAndDrop.TYPE_LE,
|
||||
lehreinheit_id: id,
|
||||
stundenblockung
|
||||
} = data;
|
||||
|
||||
return { type, id, stundenblockung };
|
||||
},
|
||||
fromVEvent(data) {
|
||||
const {
|
||||
type = DragAndDrop.TYPE_VEVENT,
|
||||
uid: id,
|
||||
dtstart,
|
||||
dtend,
|
||||
summary
|
||||
} = data;
|
||||
|
||||
return { type, id, dtstart, dtend, summary };
|
||||
}
|
||||
};
|
||||
|
||||
export default DragAndDrop;
|
||||
@@ -38720,6 +38720,68 @@ array(
|
||||
)
|
||||
),
|
||||
|
||||
//**************************** CORE/calendar
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'calendar',
|
||||
'phrase' => 'kw',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'W',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'W',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'calendar',
|
||||
'phrase' => 'year_kw',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '{year} KW {week}',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => '{year} W {week}',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'calendar',
|
||||
'phrase' => 'today',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Heute',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'today',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
//**************************** FHC-Core-SAP
|
||||
array(
|
||||
'app' => 'core',
|
||||
|
||||
Reference in New Issue
Block a user