From 869a7e33351d6904e11e53e778fc3f1253b50eea Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Tue, 21 Apr 2026 14:43:58 +0200 Subject: [PATCH] Create and implement "ClassScheduleCalendarSelector" in "ClassScheduleOverview" component --- .../api/frontend/v1/ClassScheduleApi.php | 138 +- .../v1/organisation/OrganizationalUnitApi.php | 53 + .../views/lehre/class_schedule/index.php | 7 +- public/css/components/classSchedule.css | 24 + public/js/api/factory/organizationalUnit.js | 8 + .../ClassScheduleCalendarSelector.js | 1704 +++++++++++++++++ .../ClassSchedule/ClassScheduleOverview.js | 2 +- .../ClassSchedule/ClassScheduleTypeModal.js | 33 +- .../ClassScheduleValidityPeriodForm.js | 206 +- ...ClassScheduleValidityPeriodFormTimeSlot.js | 84 - .../ClassScheduleValidityPeriodModal.js | 38 +- .../ClassScheduleValidityPeriodOverview.js | 129 +- public/js/directives/draggable.js | 3 +- public/js/helpers/DragAndDrop.js | 4 + ...031_unterrichtszeiten_der_studiengänge.php | 143 +- system/phrasesupdate.php | 480 +++++ 16 files changed, 2548 insertions(+), 508 deletions(-) create mode 100644 application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php create mode 100644 public/css/components/classSchedule.css create mode 100644 public/js/api/factory/organizationalUnit.js create mode 100644 public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js delete mode 100644 public/js/components/ClassSchedule/ClassScheduleValidityPeriodFormTimeSlot.js diff --git a/application/controllers/api/frontend/v1/ClassScheduleApi.php b/application/controllers/api/frontend/v1/ClassScheduleApi.php index 564e395c5..fb09ead03 100644 --- a/application/controllers/api/frontend/v1/ClassScheduleApi.php +++ b/application/controllers/api/frontend/v1/ClassScheduleApi.php @@ -49,7 +49,8 @@ class ClassScheduleApi extends FHCAPI_Controller // Loads phrases system $this->loadPhrases([ - 'global' + 'global', + 'ui', ]); } @@ -60,6 +61,7 @@ class ClassScheduleApi extends FHCAPI_Controller public function getAllClassTimeValidityPeriods() { $this->ClassTimeSlotValidityPeriodModel->addJoin('lehre.tbl_studienplan', 'lehre.tbl_studienplan.studienplan_id=lehre.tbl_unterrichtszeiten_gueltigkeit.studienplan_id', 'LEFT'); + $this->ClassTimeSlotValidityPeriodModel->addJoin('public.tbl_organisationseinheit', 'public.tbl_organisationseinheit.oe_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz', 'LEFT'); $this->ClassTimeSlotValidityPeriodModel->addOrder('gueltig_von', 'DESC'); $class_time_slot_validity_period_res = $this->ClassTimeSlotValidityPeriodModel->load(); $class_time_slot_validity_period_res = $this->getDataOrTerminateWithError($class_time_slot_validity_period_res); @@ -68,6 +70,8 @@ class ClassScheduleApi extends FHCAPI_Controller public function getClassTimeValidityPeriod($classTimeSlotValidityPeriodId) { + $this->ClassTimeSlotValidityPeriodModel->addSelect('lehre.tbl_unterrichtszeiten_gueltigkeit.*, public.tbl_organisationseinheit.oe_kurzbz as oe_kurzbz, lehre.tbl_studienplan.studienplan_id, lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung'); + $this->ClassTimeSlotValidityPeriodModel->addJoin('public.tbl_organisationseinheit', 'public.tbl_organisationseinheit.oe_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz', 'LEFT'); $this->ClassTimeSlotValidityPeriodModel->addJoin('lehre.tbl_studienplan', 'lehre.tbl_studienplan.studienplan_id=lehre.tbl_unterrichtszeiten_gueltigkeit.studienplan_id', 'LEFT'); $class_time_slot_validity_period_res = $this->ClassTimeSlotValidityPeriodModel->load($classTimeSlotValidityPeriodId); $class_time_slot_validity_period_res = $this->getDataOrTerminateWithError($class_time_slot_validity_period_res); @@ -76,10 +80,20 @@ class ClassScheduleApi extends FHCAPI_Controller public function createClassTimeSlotValidityPeriod() { - $this->form_validation->set_rules('validityPeriodFrom', 'Validity Period From', 'required|is_valid_date[Y-m-d]'); + $this->form_validation->set_rules('validityPeriodFrom', 'Validity Period From', 'required|is_valid_date[Y-m-d]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_validityPeriodFrom')]), + 'is_valid_date' => $this->p->t('ui', 'error_fieldInvalidDate', ['field' => $this->p->t('ui', 'field_validityPeriodFrom')]) + ]); $this->form_validation->set_rules('validityPeriodTo', 'Validity Period To', 'required|is_valid_date[Y-m-d]|callback_date_greater_equal[validityPeriodFrom]'); - $this->form_validation->set_rules('degreeProgramShortcode', 'Degree Program Shortcode', 'required|max_length[32]'); - $this->form_validation->set_rules('semester', 'Semester', 'required|is_natural_no_zero|less_than_equal_to[8]'); + $this->form_validation->set_rules('organizationalUnitShortCode', 'Organizational Unit Shortcode', 'required|max_length[32]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_organizationalUnit')]), + 'max_length' => $this->p->t('ui', 'error_fieldMaxLength', ['field' => $this->p->t('ui', 'field_organizationalUnit'), 'max' => 32]) + ]); + $this->form_validation->set_rules('semester', 'Semester', 'required|is_natural_no_zero|less_than_equal_to[8]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_semester')]), + 'is_natural_no_zero' => $this->p->t('ui', 'error_fieldInvalid', ['field' => $this->p->t('ui', 'field_semester')]), + 'less_than_equal_to' => $this->p->t('ui', 'error_fieldMaxValue', ['field' => $this->p->t('ui', 'field_semester'), 'max' => 8]) + ]); $this->form_validation->set_rules('classTimeSlotTypeShortcode', 'Class Time Slot Type Shortcode', 'max_length[32]'); $this->form_validation->set_rules('studyPlanId', 'Study Plan ID', 'is_natural_no_zero'); @@ -90,10 +104,10 @@ class ClassScheduleApi extends FHCAPI_Controller $result = $this->ClassTimeSlotValidityPeriodModel->insert([ 'gueltig_von' => $this->input->post('validityPeriodFrom'), 'gueltig_bis' => $this->input->post('validityPeriodTo'), - 'oe_kurzbz' => $this->input->post('degreeProgramShortcode'), + 'oe_kurzbz' => $this->input->post('organizationalUnitShortCode'), 'ausbildungssemester' => $this->input->post('semester'), 'anmerkung' => $this->input->post('description'), - 'unterrichtszeitentyp_kurzbz' => $this->input->post('classTimeSlotTypeShortcode') ?? '', + 'unterrichtszeitentyp_kurzbz' => $this->input->post('classTimeSlotTypeShortcode') ?? null, 'studienplan_id' => $this->input->post('studyPlanId'), 'insertamum' => date('c'), 'insertvon' => getAuthUid(), @@ -106,33 +120,45 @@ class ClassScheduleApi extends FHCAPI_Controller $this->db->trans_complete(); $this->terminateWithSuccess(true); - - $this->terminateWithSuccess($this->input->post()); } public function updateClassTimeSlotValidityPeriod($classTimeSlotValidityPeriodId) { - $this->form_validation->set_rules('validityPeriodFrom', 'Validity Period From', 'required|is_valid_date[Y-m-d]'); + $this->form_validation->set_rules('validityPeriodFrom', 'Validity Period From', 'required|is_valid_date[Y-m-d]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_validityPeriodFrom')]), + 'is_valid_date' => $this->p->t('ui', 'error_fieldInvalidDate', ['field' => $this->p->t('ui', 'field_validityPeriodFrom')]) + ]); $this->form_validation->set_rules('validityPeriodTo', 'Validity Period To', 'required|is_valid_date[Y-m-d]|callback_date_greater_equal[validityPeriodFrom]'); - $this->form_validation->set_rules('degreeProgramShortcode', 'Degree Program Shortcode', 'required|max_length[32]'); - $this->form_validation->set_rules('semester', 'Semester', 'required|is_natural_no_zero|less_than_equal_to[8]'); + $this->form_validation->set_rules('organizationalUnitShortCode', 'Organizational Unit Shortcode', 'required|max_length[32]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_organizationalUnit')]), + 'max_length' => $this->p->t('ui', 'error_fieldMaxLength', ['field' => $this->p->t('ui', 'field_organizationalUnit'), 'max' => 32]) + ]); + $this->form_validation->set_rules('semester', 'Semester', 'required|is_natural_no_zero|less_than_equal_to[8]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_semester')]), + 'is_natural_no_zero' => $this->p->t('ui', 'error_fieldInvalid', ['field' => $this->p->t('ui', 'field_semester')]), + 'less_than_equal_to' => $this->p->t('ui', 'error_fieldMaxValue', ['field' => $this->p->t('ui', 'field_semester'), 'max' => 8]) + ]); $this->form_validation->set_rules('classTimeSlotTypeShortcode', 'Class Time Slot Type Shortcode', 'max_length[32]'); $this->form_validation->set_rules('studyPlanId', 'Study Plan ID', 'is_natural_no_zero'); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); + $this->db->trans_start(); + $result = $this->ClassTimeSlotValidityPeriodModel->update($classTimeSlotValidityPeriodId, [ 'gueltig_von' => $this->input->post('validityPeriodFrom'), 'gueltig_bis' => $this->input->post('validityPeriodTo'), - 'oe_kurzbz' => $this->input->post('degreeProgramShortcode'), + 'oe_kurzbz' => $this->input->post('organizationalUnitShortCode'), 'ausbildungssemester' => $this->input->post('semester'), 'anmerkung' => $this->input->post('description'), - 'unterrichtszeitentyp_kurzbz' => $this->input->post('classTimeSlotTypeShortcode'), + 'unterrichtszeitentyp_kurzbz' => $this->input->post('classTimeSlotTypeShortcode') ?? null, 'studienplan_id' => $this->input->post('studyPlanId'), 'updateamum' => date('c'), 'updatevon' => getAuthUid(), ]); + $this->db->trans_complete(); + $data = $this->getDataOrTerminateWithError($result); $this->terminateWithSuccess(true); @@ -140,15 +166,19 @@ class ClassScheduleApi extends FHCAPI_Controller public function deleteClassTimeSlotValidityPeriod($classTimeSlotValidityPeriodId) { + $this->db->trans_start(); + + $result = $this->ClassTimeSlotModel->delete(['unterrichtszeitengueltigkeit_id'=> $classTimeSlotValidityPeriodId]); + if (isError($result)) { + $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); + } + $result = $this->ClassTimeSlotValidityPeriodModel->delete($classTimeSlotValidityPeriodId); if (isError($result)) { $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); } - $result = $this->ClassTimeSlotModel->delete(['unterrichtszeitengueltigkeit_id'=> $classTimeSlotValidityPeriodId]); - if (isError($result)) { - $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); - } + $this->db->trans_complete(); $this->terminateWithSuccess(true); } @@ -163,14 +193,14 @@ class ClassScheduleApi extends FHCAPI_Controller public function createClassTimeSlotsForValidityPeriod($classTimeSlotValidityPeriodId) { - $this->form_validation->set_rules('classTimeSlots', 'Validity Period From', 'callback_validate_items_in_class_time_slots'); + $this->form_validation->set_rules('unterrichtszeiten', 'Class Time Slots', 'callback_validate_items_in_class_time_slots'); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); $this->db->trans_start(); $timeSlotGroupIdentifier = uniqid(); - foreach ($this->input->post('classTimeSlots') as $timeSlot) { + foreach ($this->input->post('unterrichtszeiten') as $timeSlot) { $result = $this->ClassTimeSlotModel->insert([ 'unterrichtszeit_gruppe_identifikator' => $timeSlotGroupIdentifier, 'wochentag' => $timeSlot['wochentag'], @@ -193,14 +223,14 @@ class ClassScheduleApi extends FHCAPI_Controller public function editClassTimeSlotsForValidityPeriod($classTimeSlotValidityPeriodId) { - $this->form_validation->set_rules('classTimeSlots', 'Validity Period From', 'callback_validate_items_in_class_time_slots'); + $this->form_validation->set_rules('unterrichtszeiten', 'Class Time Slots', 'callback_validate_items_in_class_time_slots'); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); $this->db->trans_start(); $timeSlotGroupIdentifier = uniqid(); - foreach ($this->input->post('classTimeSlots') as $timeSlot) { + foreach ($this->input->post('unterrichtszeiten') as $timeSlot) { $data = [ 'unterrichtszeit_gruppe_identifikator' => $timeSlotGroupIdentifier, 'wochentag' => $timeSlot['wochentag'], @@ -229,11 +259,15 @@ class ClassScheduleApi extends FHCAPI_Controller } public function deleteClassTimeSlotsForValidityPeriodPerGroup($classTimeSlotValidityPeriodId, $groupIdentifikator) { + $this->db->trans_start(); + $result = $this->ClassTimeSlotModel->delete(['unterrichtszeitengueltigkeit_id'=> $classTimeSlotValidityPeriodId, 'unterrichtszeit_gruppe_identifikator' => $groupIdentifikator]); if (isError($result)) { $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); } + $this->db->trans_complete(); + $this->terminateWithSuccess(true); } @@ -255,8 +289,15 @@ class ClassScheduleApi extends FHCAPI_Controller public function createClassTimeSlotType() { - $this->form_validation->set_rules('shortCode', 'Short Code', 'required|max_length[32]'); + $this->form_validation->set_rules('shortCode', 'Short Code', 'required|max_length[32]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_classTimeSlotTypeShortCode')]), + 'max_length' => $this->p->t('ui', 'error_fieldMaxLength', ['field' => $this->p->t('ui', 'field_classTimeSlotTypeShortCode'), 'max' => 32]) + ]); $this->form_validation->set_rules('descriptions', 'Descriptions', 'callback_validate_descriptions_array'); + $this->form_validation->set_rules('backgroundColor', 'Background Color', 'required|regex_match[/^#([0-9a-fA-F]{3}){1,2}$/]', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_backgroundColor')]), + 'regex_match' => $this->p->t('ui', 'error_fieldInvalid', ['field' => $this->p->t('ui', 'field_backgroundColor')]), + ]); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); @@ -268,14 +309,16 @@ class ClassScheduleApi extends FHCAPI_Controller $query = 'INSERT INTO lehre.tbl_unterrichtszeiten_typ ( unterrichtszeitentyp_kurzbz, bezeichnung_mehrsprachig, + hintergrundfarbe, aktiv, insertamum, insertvon, updateamum, - updatevon) VALUES (?, ?, ?, ?, ?, ?, ?)'; + updatevon) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; $result = $this->db->query($query, [ $this->input->post('shortCode'), $pgArray, + $this->input->post('backgroundColor'), $this->input->post('isActive'), date('c'), getAuthUid(), @@ -298,9 +341,10 @@ class ClassScheduleApi extends FHCAPI_Controller $descriptions = $this->input->post('descriptions'); $pgArray = $this->arrayToPgArray($descriptions); - $query = 'UPDATE lehre.tbl_unterrichtszeiten_typ SET bezeichnung_mehrsprachig = ?, aktiv = ?, updateamum = ?, updatevon = ? WHERE unterrichtszeitentyp_kurzbz = ?'; + $query = 'UPDATE lehre.tbl_unterrichtszeiten_typ SET bezeichnung_mehrsprachig = ?, hintergrundfarbe = ?, aktiv = ?, updateamum = ?, updatevon = ? WHERE unterrichtszeitentyp_kurzbz = ?'; $result = $this->db->query($query, [ $pgArray, + $this->input->post('backgroundColor'), $this->input->post('isActive'), date('c'), getAuthUid(), @@ -313,11 +357,15 @@ class ClassScheduleApi extends FHCAPI_Controller public function deleteClassTimeSlotType($classTimeSlotTypeId) { + $this->db->trans_start(); + $result = $this->ClassTimeSlotTypeModel->delete(['unterrichtszeitentyp_kurzbz' => $classTimeSlotTypeId]); if (isError($result)) { $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); } - + + $this->db->trans_complete(); + $this->terminateWithSuccess(true); } //------------------------------------------------------------------------------------------------------------------ @@ -339,29 +387,26 @@ class ClassScheduleApi extends FHCAPI_Controller { $fromDate = $this->input->post($fromField); - // Check if "from" exists if (!$fromDate) { $this->form_validation->set_message( 'date_greater_equal', - 'Validity Period From is required.' + $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_validityPeriodTo')]) ); return false; } - // Validate both dates if (!strtotime($toDate) || !strtotime($fromDate)) { $this->form_validation->set_message( 'date_greater_equal', - 'Both dates must be valid.' + $this->p->t('ui', 'error_fieldInvalidDate', ['field' => $this->p->t('ui', 'field_validityPeriodTo')]) ); return false; } - // Compare dates if (strtotime($toDate) < strtotime($fromDate)) { $this->form_validation->set_message( 'date_greater_equal', - 'The {field} must be greater than or equal to Validity Period From.' + $this->p->t('ui', 'error_fieldDateGreaterEqual', ['field' => $this->p->t('ui', 'field_validityPeriodTo'), 'otherField' => $this->p->t('ui', 'field_validityPeriodFrom')]) ); return false; } @@ -369,41 +414,38 @@ class ClassScheduleApi extends FHCAPI_Controller return true; } - public function validate_items_in_class_time_slots($classTimeSlots) + public function validate_items_in_class_time_slots($unterrichtszeiten) { - // see if $classTimeSlots is an array and has at least one item - - if (!is_array($this->input->post('classTimeSlots')) || count($this->input->post('classTimeSlots')) === 0) { + if (!is_array($this->input->post('unterrichtszeiten')) || count($this->input->post('unterrichtszeiten')) === 0) { $this->form_validation->set_message( 'validate_items_in_class_time_slots', - 'At least one class time slot is required.' + $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_classTimeSlot')]) ); return false; } - foreach ($this->input->post('classTimeSlots') as $index => $timeSlot) { + foreach ($this->input->post('unterrichtszeiten') as $index => $timeSlot) { if (!isset($timeSlot['wochentag'], $timeSlot['startTime'], $timeSlot['endTime'], $timeSlot['classTimeSlotTypeShortcode'])) { $this->form_validation->set_message( 'validate_items_in_class_time_slots', - 'Each class time slot must have a weekday, start time, end time and class time slot type shortcode.' + $this->p->t('ui', 'error_fieldClassTimeSlotContentInvalid', ['field' => $this->p->t('ui', 'field_classTimeSlot')]) ); return false; } - if (!in_array($timeSlot['wochentag'], [1, 2, 3, 4, 5])) { + if (!in_array($timeSlot['wochentag'], [1, 2, 3, 4, 5, 6, 7])) { $this->form_validation->set_message( 'validate_items_in_class_time_slots', - 'Weekday must be an integer between 1 (Monday) and 5 (Friday).' + $this->p->t('ui', 'error_fieldWeekdayInvalid', ['field' => $this->p->t('ui', 'field_classTimeSlot')]) ); return false; } - log_message('error', 'Validating class time slots: ' . print_r($timeSlot['startTime'], true)); if (!strtotime($timeSlot['startTime']) || !strtotime($timeSlot['endTime'])) { $this->form_validation->set_message( 'validate_items_in_class_time_slots', - 'Start time and end time must be valid time strings.' + $this->p->t('ui', 'error_fieldClassTimeSlotTimeInvalid', ['field' => $this->p->t('ui', 'field_classTimeSlot')]) ); return false; } @@ -411,14 +453,14 @@ class ClassScheduleApi extends FHCAPI_Controller if (strtotime($timeSlot['endTime']) <= strtotime($timeSlot['startTime'])) { $this->form_validation->set_message( 'validate_items_in_class_time_slots', - 'End time must be greater than start time.' + $this->p->t('ui', 'error_fieldDateGreaterEqual', ['field' => $this->p->t('ui', 'field_classTimeSlotEndTime'), 'otherField' => $this->p->t('ui', 'field_classTimeSlotStartTime')]) ); return false; } } $slotsByDay = []; - foreach ($this->input->post('classTimeSlots') as $timeSlot) { + foreach ($this->input->post('unterrichtszeiten') as $timeSlot) { $slotsByDay[$timeSlot['wochentag']][] = $timeSlot; } @@ -431,7 +473,7 @@ class ClassScheduleApi extends FHCAPI_Controller if (strtotime($slots[$i]['startTime']) < strtotime($slots[$i - 1]['endTime'])) { $this->form_validation->set_message( 'validate_items_in_class_time_slots', - 'Class time slots for each day must not overlap.' + $this->p->t('ui', 'error_fieldClassTimeSlotOverlap', ['field' => $this->p->t('ui', 'field_classTimeSlot')]) ); return false; } @@ -448,7 +490,7 @@ class ClassScheduleApi extends FHCAPI_Controller if (!is_array($descriptions) || count($descriptions) === 0) { $this->form_validation->set_message( 'validate_descriptions_array', - 'Descriptions must be a non-empty array.' + $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('ui', 'field_descriptions')]) ); return false; } @@ -457,7 +499,7 @@ class ClassScheduleApi extends FHCAPI_Controller if (!isset($description['lang'], $description['value'])) { $this->form_validation->set_message( 'validate_descriptions_array', - 'Each description must have a language and a value.' + $this->p->t('ui', 'error_fieldDescriptionContentInvalid') ); return false; } @@ -465,7 +507,7 @@ class ClassScheduleApi extends FHCAPI_Controller if (empty($description['lang']) || empty($description['value'])) { $this->form_validation->set_message( 'validate_descriptions_array', - 'Language and value in each description must not be empty.' + $this->p->t('ui', 'error_fieldDescriptionContentInvalid') ); return false; } diff --git a/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php b/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php new file mode 100644 index 000000000..6fa5a50f8 --- /dev/null +++ b/application/controllers/api/frontend/v1/organisation/OrganizationalUnitApi.php @@ -0,0 +1,53 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); +class OrganizationalUnitApi extends FHCAPI_Controller +{ + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'getAllOrganizationalUnits'=> array('basis/organisationseinheit:r'), + ]); + + $this->load->library('form_validation'); + + $this->load->model('education/ClassTimeSlotValidityPeriod_model', "ClassTimeSlotValidityPeriodModel"); + + // Loads phrases system + $this->loadPhrases([ + 'global' + ]); + + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + public function getAllOrganizationalUnits() + { + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $result = $this->OrganisationseinheitModel->load(); + $organization_units_result = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($organization_units_result); + } +} diff --git a/application/views/lehre/class_schedule/index.php b/application/views/lehre/class_schedule/index.php index 3cd56b2cd..7d00e8c9c 100644 --- a/application/views/lehre/class_schedule/index.php +++ b/application/views/lehre/class_schedule/index.php @@ -9,18 +9,19 @@ $includesArray = array( 'primevue3' => true, 'navigationcomponent' => true, 'filtercomponent' => true, + 'vuedatepicker11' => true, 'customJSs' => array( - 'vendor/vuejs/vuedatepicker_js/vue-datepicker.iife.js', 'vendor/moment/luxonjs/luxon.min.js' ), 'customJSModules' => array( 'public/js/apps/lehre/ClassScheduleApp.js' ), 'customCSSs' => array( - 'public/css/components/vue-datepicker.css', 'public/css/components/primevue.css', 'public/css/components/verticalsplit.css', - 'public/extensions/FHC-Core-Developer/css/FhcMain.css' + 'public/extensions/FHC-Core-Developer/css/FhcMain.css', + 'public/css/components/calendar.css', + 'public/css/components/classSchedule.css' ) ); diff --git a/public/css/components/classSchedule.css b/public/css/components/classSchedule.css new file mode 100644 index 000000000..de2f9a109 --- /dev/null +++ b/public/css/components/classSchedule.css @@ -0,0 +1,24 @@ +.fhc-pointer-events-none { + pointer-events: none; +} + +.fhc-pointer-events-all { + pointer-events: all; +} + +.fhc-cursor-pointer { + cursor: pointer; +} + +.fhc-resize-vertical { + cursor: ns-resize; +} + +.fhc-drag-handle:hover { + cursor: grab; + color: #000; +} + +.fhc-w-fit { + width: fit-content; +} \ No newline at end of file diff --git a/public/js/api/factory/organizationalUnit.js b/public/js/api/factory/organizationalUnit.js new file mode 100644 index 000000000..1c17ab5d1 --- /dev/null +++ b/public/js/api/factory/organizationalUnit.js @@ -0,0 +1,8 @@ +export default { + getAllOrganizationalUnits() { + return { + method: "get", + url: "api/frontend/v1/organisation/organizationalUnitApi/getAllOrganizationalUnits", + }; + }, +} \ No newline at end of file diff --git a/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js b/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js new file mode 100644 index 000000000..9bf7ebb2b --- /dev/null +++ b/public/js/components/ClassSchedule/ClassScheduleCalendarSelector.js @@ -0,0 +1,1704 @@ +import draggable from "../../directives/draggable.js"; +import drop from "../../directives/drop.js"; +import FormInput from "../Form/Input.js"; + +export default { + name: "ClassScheduleCalendarSelector", + components: { + FormInput, + }, + directives: { + draggable, + drop, + }, + props: { + isPreviewMode: { + type: Boolean, + required: false, + default: false, + }, + classTimeSlotTypes: { + type: [Array, null], + required: true, + }, + editedOverlays: { + type: [Array, null], + required: false, + default: () => [], + }, + }, + emits: ["overlaysChanged"], + watch: { + editedOverlays: { + handler(newVal) { + setTimeout(() => { + this.overlays = []; + + this.$refs.calendarContainer + .querySelectorAll("div[id^='overlay-']") + .forEach((element) => { + element.remove(); + }); + newVal + .map((slot) => { + return { + ...slot, + startingTimeSlot: this.timeSlotsInDay.find((timeSlot) => + timeSlot.startsWith(slot.startTime.substr(0, 5)), + ), + endingTimeSlot: this.timeSlotsInDay.find((timeSlot) => + timeSlot.endsWith(slot.endTime.substr(0, 5)), + ), + }; + }) + .forEach((overlay) => { + let firstElementDataNumber = + (overlay.weekday - 1) * this.timeSlotsInDay.length + + this.timeSlotsInDay.indexOf(overlay.startingTimeSlot); + let lastElementDataNumber = + (overlay.weekday - 1) * this.timeSlotsInDay.length + + this.timeSlotsInDay.indexOf(overlay.endingTimeSlot); + + let firstSelectedElement = + this.$refs.calendarSelectorContainer.querySelector( + "div[data-number='" + firstElementDataNumber + "']", + ); + let lastSelectedElement = + this.$refs.calendarSelectorContainer.querySelector( + "div[data-number='" + lastElementDataNumber + "']", + ); + if (!firstSelectedElement || !lastSelectedElement) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotLoadingErrorMessage"), + ); + return; + } + + firstSelectedElement.style.backgroundColor = + this.selectedTimeSlotLabelColor; + lastSelectedElement.style.backgroundColor = + this.selectedTimeSlotLabelColor; + + this.currentFirstSelectedElementNumber = firstSelectedElement + ? parseInt(firstSelectedElement.getAttribute("data-number")) + : null; + + this.currentLastSelectedElementNumber = lastSelectedElement + ? parseInt(lastSelectedElement.getAttribute("data-number")) + : null; + + this.createOverlay(); + + this.$refs.calendarSelectorContainer + .querySelectorAll("div[class*='part-body']") + .forEach((child) => { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + }); + + this.currentFirstSelectedElementNumber = null; + this.currentLastSelectedElementNumber = null; + }); + + this.overlays = this.overlays.map((existingOverlay, index) => { + let existingOverlayInNewVal = newVal[index]; + + existingOverlay.databaseId = existingOverlayInNewVal.databaseId; + existingOverlay.type = existingOverlayInNewVal.type; + existingOverlay.hexColor = + this.classTimeSlotTypes.find( + (type) => + type.unterrichtszeitentyp_kurzbz === + existingOverlayInNewVal.type, + )?.hintergrundfarbe || null; + return { + ...existingOverlay, + }; + }); + }, 500); + }, + deep: true, + immediate: true, + }, + overlays: { + handler(newVal) { + this.$emit("overlaysChanged", newVal); + }, + deep: true, + }, + }, + data() { + return { + daysInWeek: [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ], + timeSlotsInDay: [ + "08:00-08:45", + "08:45-09:30", + "09:40-10:25", + "10:25-11:10", + "11:20-12:05", + "12:05-12:50", + "12:50-13:35", + "13:35-14:20", + "14:30-15:15", + "15:15-16:00", + "16:10-16:55", + "16:55-17:40", + "17:50-18:35", + "18:35-19:20", + "19:30-20:15", + "20:15-21:00", + ], + isTimeElementCreationInProgress: false, + overlays: [], + defaultTimeSlotLabelColor: "var(--fhc-calendar-bg-body)", + selectedTimeSlotLabelColor: "rgb(236, 235, 189)", + defaultOverlayColor: "rgb(232, 226, 226)", + overlayTextColor: "rgb(0, 0, 0)", + currentFirstSelectedElementNumber: null, + currentLastSelectedElementNumber: null, + selected: [], + isTimeElementResizingInProgress: false, + currentResizer: { + id: null, + overlayId: null, + }, + oldMousePosition: { + x: null, + y: null, + }, + }; + }, + computed: { + selectedDragObject() { + return this.selected.map((item) => ({ + name: "test", + type: "calendar_selector_overlay", + id: 2, + })); + }, + currentlySelectedTimeSlotSpan() { + if ( + !this.currentFirstSelectedElementNumber || + !this.currentLastSelectedElementNumber + ) + return null; + + const firstTimeSlot = + this.timeSlotsInDay[ + this.currentFirstSelectedElementNumber % this.timeSlotsInDay.length + ]; + const lastTimeSlot = + this.timeSlotsInDay[ + this.currentLastSelectedElementNumber % this.timeSlotsInDay.length + ]; + + let firstTimeSlotFragment = firstTimeSlot.split("-")[0]; + let lastTimeSlotFragment = lastTimeSlot.split("-")[1]; + + return firstTimeSlotFragment + "-" + lastTimeSlotFragment; + }, + }, + methods: { + createOverlay() { + let overlayElement; + + if (!this.$props.isPreviewMode) { + overlayElement = this.$refs.calendarSelectorContainer.querySelector( + "#overlays-container", + ).children[0]; + } else { + overlayElement = this.$refs.calendarSelectorContainer.querySelector( + "#overlays-container-preview", + ).children[0]; + } + + let firstSelectedChild = + this.$refs.calendarSelectorContainer.querySelector( + `div[style*='background-color: ${this.selectedTimeSlotLabelColor};']`, + ); + + let lastSelectedChild = + this.$refs.calendarSelectorContainer.querySelectorAll( + `div[style*='background-color: ${this.selectedTimeSlotLabelColor};']`, + ); + lastSelectedChild = lastSelectedChild[lastSelectedChild.length - 1]; + + if (!firstSelectedChild || !lastSelectedChild) { + return; + } + + let firstSelectedElementNumber = parseInt( + firstSelectedChild.getAttribute("data-number"), + ); + let lastSelectedElementNumber = parseInt( + lastSelectedChild.getAttribute("data-number"), + ); + if ( + firstSelectedElementNumber === null || + lastSelectedElementNumber === null + ) { + console.error("Selected elements do not have data-number attribute"); + return; + } + + this.currentFirstSelectedElementNumber = firstSelectedElementNumber; + this.currentLastSelectedElementNumber = lastSelectedElementNumber; + + let gridLineNumber; + try { + gridLineNumber = this.getLineNumberFromSelectedElementsNumbers( + firstSelectedElementNumber, + lastSelectedElementNumber, + ); + } catch (error) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotMultipleWeeksSelectedErrorMessage"), + ); + + return; + } + + let gridLine = this.$refs.calendarSelectorContainer.querySelectorAll( + ".fhc-calendar-base-grid-line", + )[gridLineNumber]; + if (!gridLine) return; + + overlayElement.classList.remove("d-none"); + overlayElement.style.display = "flex"; + overlayElement.style.position = "absolute"; + overlayElement.style.top = firstSelectedChild.offsetTop + "px"; + overlayElement.style.left = "0px"; + overlayElement.style.width = "100%"; + overlayElement.style.height = + lastSelectedChild.offsetTop + + lastSelectedChild.offsetHeight - + firstSelectedChild.offsetTop + + "px"; + overlayElement.style.backgroundColor = this.defaultOverlayColor; + + if ( + this.currentFirstSelectedElementNumber > + this.currentLastSelectedElementNumber + ) { + let temp = this.currentFirstSelectedElementNumber; + this.currentFirstSelectedElementNumber = + this.currentLastSelectedElementNumber; + this.currentLastSelectedElementNumber = temp; + } + + let overlayId = overlayElement.id; + + let hasCollidingOverlays = this.overlays.some((overlay) => { + if (overlay.weekday !== gridLineNumber + 1) return false; + if (overlay.id === overlayId) return false; + if ( + (overlay.startingTimeSlotElementNumber <= + this.currentFirstSelectedElementNumber && + overlay.endingTimeSlotElementNumber >= + this.currentFirstSelectedElementNumber) || + (overlay.startingTimeSlotElementNumber <= + this.currentLastSelectedElementNumber && + overlay.endingTimeSlotElementNumber >= + this.currentLastSelectedElementNumber) + ) { + return true; + } + return false; + }); + + if (hasCollidingOverlays) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotOverlapErrorMessage"), + ); + + this.currentFirstSelectedElementNumber = null; + this.currentLastSelectedElementNumber = null; + return; + } + + gridLine.appendChild(overlayElement); + + if (!this.overlays.some((overlay) => overlay.id === overlayId)) { + this.overlays.push({ + id: overlayId, + startingTimeSlotElementNumber: this.currentFirstSelectedElementNumber, + endingTimeSlotElementNumber: this.currentLastSelectedElementNumber, + startingTimeSlot: + this.timeSlotsInDay[ + this.currentFirstSelectedElementNumber % + this.timeSlotsInDay.length + ], + endingTimeSlot: + this.timeSlotsInDay[ + this.currentLastSelectedElementNumber % this.timeSlotsInDay.length + ], + type: null, + hexColor: null, + weekday: gridLineNumber + 1, + }); + } + }, + deleteOverlay(overlayId) { + let confirm = window.confirm( + this.$p.t("ui", "classTimeSlotDeletionConfirmationMessage"), + ); + if (!confirm) return; + + this.overlays = this.overlays.filter( + (overlay) => overlay.id !== overlayId, + ); + + this.$refs.calendarSelectorContainer + .querySelector(`#${overlayId}`) + .remove(); + }, + getLineNumberFromSelectedElementNumber(selectedElementNumber) { + let timeSlotsCount = this.timeSlotsInDay.length; + + let gridLineNumber = parseInt(selectedElementNumber / timeSlotsCount); + + return gridLineNumber; + }, + getLineNumberFromSelectedElementsNumbers( + firstSelectedElementNumber, + lastSelectedElementNumber, + ) { + let timeSlotsCount = this.timeSlotsInDay.length; + + let firstNumberGridLine = parseInt( + firstSelectedElementNumber / timeSlotsCount, + ); + let lastNumberGridLine = parseInt( + lastSelectedElementNumber / timeSlotsCount, + ); + + if (firstNumberGridLine !== lastNumberGridLine) { + throw new Error("Selected elements are not in the same grid line"); + } + + return firstNumberGridLine; + }, + getOverlayTimeSlotSpan(overlayId) { + let overlay = this.overlays.find((overlay) => overlay.id === overlayId); + if (!overlay) return null; + + let startingTimeSlotFragment = overlay.startingTimeSlot.split("-")[0]; + let endingTimeSlotFragment = overlay.endingTimeSlot.split("-")[1]; + + return startingTimeSlotFragment + " - " + endingTimeSlotFragment; + }, + handleMouseDown(event) { + if (this.$props.isPreviewMode) return; + + let isLeftMouseButton = event.buttons === 1; + if (!isLeftMouseButton) return; + + this.isTimeElementCreationInProgress = true; + + let parent = + this.$refs.calendarSelectorContainer.querySelector(".grid-body"); + if (!parent.contains(event.target)) { + this.isTimeElementCreationInProgress = false; + return; + } + + let mouseY = event.clientY; + let mouseX = event.clientX; + + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( + `div[class*='part-body']`, + ); + + let closestXPartBody = null; + let closestXDistance = Infinity; + let weekday = null; + partBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + const distanceX = Math.abs(mouseX - rect.left - rect.width / 2); + + let gridLineHeight = this.$refs.calendarSelectorContainer.querySelector( + ".fhc-calendar-base-grid-line", + ).style.height; + let seeIfAdjacentElementIsGridLine = + partBody.nextElementSibling.classList.contains( + "fhc-calendar-base-grid-line", + ); + if ( + distanceX < closestXDistance || + (distanceX + gridLineHeight === closestXDistance && + seeIfAdjacentElementIsGridLine) + ) { + closestXDistance = distanceX; + closestXPartBody = partBody; + } + }); + + if (closestXPartBody) { + let number = parseInt(closestXPartBody.getAttribute("data-number")); + weekday = parseInt(number / this.timeSlotsInDay.length) + 1; + } + + if (!weekday) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotSelectionErrorMessage"), + ); + this.isTimeElementCreationInProgress = false; + return; + } + + let closestPartBody = null; + let closestYDistance = Infinity; + const newPartBodies = + this.$refs.calendarSelectorContainer.querySelectorAll( + `div[data-weekday='${weekday}']`, + ); + newPartBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + const distanceY = Math.abs(mouseY - rect.top - rect.height / 2); + + let gridLineHeight = this.$refs.calendarSelectorContainer.querySelector( + ".fhc-calendar-base-grid-line", + ).style.height; + let seeIfAdjacentElementIsGridLine = + partBody.nextElementSibling.classList.contains( + "fhc-calendar-base-grid-line", + ); + if ( + distanceY < closestYDistance || + (distanceY + gridLineHeight === closestYDistance && + seeIfAdjacentElementIsGridLine) + ) { + closestYDistance = distanceY; + closestPartBody = partBody; + } + }); + + if (closestPartBody) { + let number = parseInt(closestPartBody.getAttribute("data-number")); + this.currentFirstSelectedElementNumber = number; + closestPartBody.style.backgroundColor = this.selectedTimeSlotLabelColor; + } + }, + handleMouseMove(event) { + if (this.$props.isPreviewMode) return; + if (!this.isTimeElementCreationInProgress) return; + + let weekday = + parseInt( + this.currentFirstSelectedElementNumber / this.timeSlotsInDay.length, + ) + 1; + let mouseY = event.clientY; + + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( + `div[data-weekday='${weekday}']`, + ); + let closestPartBody = null; + let closestDistance = Infinity; + + partBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + const distance = Math.abs(mouseY - rect.top - rect.height / 2); + + let gridLineHeight = this.$refs.calendarSelectorContainer.querySelector( + ".fhc-calendar-base-grid-line", + ).style.height; + let seeIfAdjacentElementIsGridLine = + partBody.nextElementSibling.classList.contains( + "fhc-calendar-base-grid-line", + ); + if ( + distance < closestDistance || + (distance + gridLineHeight === closestDistance && + seeIfAdjacentElementIsGridLine) + ) { + closestDistance = distance; + closestPartBody = partBody; + } + }); + + if (closestPartBody) { + this.$refs.calendarSelectorContainer + .querySelectorAll(`div[data-weekday='${weekday}']`) + .forEach((child) => { + let itemNumber = parseInt(child.getAttribute("data-number")); + + if ( + itemNumber >= this.currentFirstSelectedElementNumber && + itemNumber <= + parseInt(closestPartBody.getAttribute("data-number")) + ) { + child.style.backgroundColor = this.selectedTimeSlotLabelColor; + } else if ( + itemNumber <= this.currentFirstSelectedElementNumber && + itemNumber >= + parseInt(closestPartBody.getAttribute("data-number")) + ) { + child.style.backgroundColor = this.selectedTimeSlotLabelColor; + } else { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + } + }); + } + }, + handleMouseUp(event) { + if (this.$props.isPreviewMode) return; + + let isLeftMouseButton = event.buttons === 0; + if (!isLeftMouseButton) return; + + if (!this.isTimeElementCreationInProgress) { + this.$refs.calendarSelectorContainer + .querySelectorAll("div[id^='overlay-']") + .forEach((element) => { + element.classList.remove("fhc-pointer-events-none"); + element.classList.add("fhc-pointer-events-all"); + }); + this.isTimeElementCreationInProgress = false; + return; + } + + if (this.isTimeElementResizingInProgress) { + return; + } + + this.isTimeElementCreationInProgress = false; + + let weekday = + parseInt( + this.currentFirstSelectedElementNumber / this.timeSlotsInDay.length, + ) + 1; + let mouseY = event.clientY; + + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( + `div[data-weekday='${weekday}']`, + ); + let closestPartBody = null; + let closestDistance = Infinity; + + partBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + const distance = Math.abs(mouseY - rect.top - rect.height / 2); + + let gridLineHeight = this.$refs.calendarSelectorContainer.querySelector( + ".fhc-calendar-base-grid-line", + ).style.height; + let seeIfAdjacentElementIsGridLine = + partBody.nextElementSibling.classList.contains( + "fhc-calendar-base-grid-line", + ); + if ( + distance < closestDistance || + (distance + gridLineHeight === closestDistance && + seeIfAdjacentElementIsGridLine) + ) { + closestDistance = distance; + closestPartBody = partBody; + } + }); + + if (closestPartBody) { + this.$refs.calendarSelectorContainer + .querySelectorAll(`div[data-weekday='${weekday}']`) + .forEach((child) => { + let itemNumber = parseInt(child.getAttribute("data-number")); + + if ( + itemNumber >= this.currentFirstSelectedElementNumber && + itemNumber <= + parseInt(closestPartBody.getAttribute("data-number")) + ) { + child.style.backgroundColor = this.selectedTimeSlotLabelColor; + } else if ( + itemNumber <= this.currentFirstSelectedElementNumber && + itemNumber >= + parseInt(closestPartBody.getAttribute("data-number")) + ) { + child.style.backgroundColor = this.selectedTimeSlotLabelColor; + } else { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + } + }); + } + + this.createOverlay(); + + this.$refs.calendarSelectorContainer + .querySelectorAll("div[class*='part-body']") + .forEach((child) => { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + }); + + this.$refs.calendarSelectorContainer + .querySelectorAll("div[id^='overlay-']") + .forEach((element) => { + element.classList.remove("fhc-pointer-events-none"); + element.classList.add("fhc-pointer-events-all"); + }); + + this.currentFirstSelectedElementNumber = null; + this.currentLastSelectedElementNumber = null; + }, + handleLeave(event) { + if (this.$props.isPreviewMode) return; + + if (!this.isTimeElementCreationInProgress) return; + const rect = event.target.getBoundingClientRect(); + + let hasLeftFromTop = undefined; + const fromTop = event.clientY <= rect.top + 15; + + if (fromTop) { + hasLeftFromTop = true; + } else { + hasLeftFromTop = false; + } + + const child = event.target; + let number = parseInt(child.getAttribute("data-number")); + + if (number > this.currentFirstSelectedElementNumber) { + if (hasLeftFromTop) { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + } + } else if (number < this.currentFirstSelectedElementNumber) { + if (!hasLeftFromTop) { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + } + } + }, + overlaySelectionChanged(event, overlayId) { + if (this.$props.isPreviewMode) return; + + let isLeftMouseButton = event.buttons === 1; + if (!isLeftMouseButton) return; + + this.$refs.calendarSelectorContainer + .querySelector(`#${overlayId}`) + .classList.remove("fhc-pointer-events-all"); + this.$refs.calendarSelectorContainer + .querySelector(`#${overlayId}`) + .classList.add("fhc-pointer-events-none"); + this.selected = [ + { + type: "calendar_selector_overlay", + id: overlayId, + }, + ]; + }, + handleMouseUpOnOverlay(overlayId) { + if (this.$props.isPreviewMode) return; + + this.$refs.calendarSelectorContainer + .querySelector(`#${overlayId}`) + .classList.remove("fhc-pointer-events-none"); + this.$refs.calendarSelectorContainer + .querySelector(`#${overlayId}`) + .classList.add("fhc-pointer-events-all"); + this.selected = []; + }, + handleOverlayDrop(event) { + if (this.$props.isPreviewMode) return; + + let dropzoneItem = event.target; + if (!dropzoneItem) return; + + + if (!dropzoneItem.getAttribute("data-time")) { + let dropzoneOverlay = this.overlays.find((overlay) => overlay.id === dropzoneItem.id); + if (!dropzoneOverlay) { + console.error("Could not find overlay for dropzone item with id " + dropzoneItem.id); + return; + } + + if (dropzoneOverlay.id !== this.selected[0].id) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotOverlapErrorMessage"), + ); + return; + } + + let startElementNumber = dropzoneOverlay.startingTimeSlotElementNumber; + let startElement = this.$refs.calendarSelectorContainer.querySelector( + `[data-number='${startElementNumber}']` + ); + + // get mouse position + const mouseY = event.clientY; + // get delta Y from mouse position to top of the dropzone item + const dropzoneItemRect = dropzoneItem.getBoundingClientRect(); + const deltaY = mouseY - dropzoneItemRect.top; + + // get top of the start element + const startElementRect = startElement.getBoundingClientRect(); + const startElementTop = startElementRect.top; + + // add delta Y to top of the start element to get new top for the dropzone item + const newTop = startElementTop + deltaY; + //find which item has the closest top to the new top and get its data-number attribute + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( + "div[data-weekday='" + dropzoneOverlay.weekday + "']", + ); + + // see which item has the new top in between its top and bottom and get its data-number attribute + let closestPartBody = null; + partBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + if (newTop >= rect.top && newTop <= rect.bottom) { + closestPartBody = partBody; + } + }); + if (!closestPartBody) return; + + dropzoneItem = closestPartBody; + } + + let newStartTimeSlot = + this.timeSlotsInDay[ + dropzoneItem.getAttribute("data-number") % this.timeSlotsInDay.length + ]; + + const draggedItemId = + this.selected.length > 0 ? this.selected[0].id : null; + if (!draggedItemId) return; + + const draggedItem = this.$refs.calendarSelectorContainer.querySelector( + `#${draggedItemId}`, + ); + if (!draggedItem) return; + + let overlay = this.overlays.find( + (overlay) => overlay.id === draggedItemId, + ); + if (!overlay) return; + + let gridLineNumber; + try { + gridLineNumber = this.getLineNumberFromSelectedElementNumber( + dropzoneItem.getAttribute("data-number"), + ); + } catch (error) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotMultipleWeeksSelectedErrorMessage"), + ); + + return; + } + + let gridLine = this.$refs.calendarSelectorContainer.querySelectorAll( + ".fhc-calendar-base-grid-line", + )[gridLineNumber]; + if (!gridLine) return; + + let overlayElement = this.$refs.calendarSelectorContainer.querySelector( + `#${draggedItemId}`, + ); + gridLine.appendChild(overlayElement); + + draggedItem.style.top = dropzoneItem.offsetTop + "px"; + let draggedItemRectBottom = draggedItem.getBoundingClientRect().bottom; + + let weekday = gridLineNumber + 1; + + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( + "div[data-weekday='" + weekday + "']", + ); + let closestPartBody = null; + let closestDistance = Infinity; + + partBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + const distance = Math.abs( + draggedItemRectBottom - rect.top - rect.height / 2, + ); + + let gridLineHeight = this.$refs.calendarSelectorContainer.querySelector( + ".fhc-calendar-base-grid-line", + ).style.height; + let seeIfAdjacentElementIsGridLine = + partBody.nextElementSibling.classList.contains( + "fhc-calendar-base-grid-line", + ); + if ( + distance < closestDistance || + (distance + gridLineHeight === closestDistance && + seeIfAdjacentElementIsGridLine) + ) { + closestDistance = distance; + closestPartBody = partBody; + } + }); + + if (closestPartBody) { + const closestPartBodyNumber = parseInt( + closestPartBody.getAttribute("data-number"), + ); + + const rect = closestPartBody.getBoundingClientRect(); + const deltaY = rect.bottom - draggedItemRectBottom; + + draggedItem.style.height = + parseInt(draggedItem.style.height) + deltaY + "px"; + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === draggedItemId) { + const timeSlot = + this.timeSlotsInDay[ + closestPartBodyNumber % this.timeSlotsInDay.length + ]; + overlay.endingTimeSlotElementNumber = closestPartBodyNumber; + overlay.endingTimeSlot = timeSlot; + + overlay.weekday = gridLineNumber + 1; + return overlay; + } + return overlay; + }); + } + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === draggedItemId) { + overlay.startingTimeSlotElementNumber = parseInt( + dropzoneItem.getAttribute("data-number"), + ); + overlay.startingTimeSlot = newStartTimeSlot; + return overlay; + } + return overlay; + }); + + let firstSkippedOverElement = this.overlays.find((innerOverlay) => { + if (innerOverlay.id === overlay.id) return false; + + if ( + overlay.startingTimeSlotElementNumber <= + innerOverlay.startingTimeSlotElementNumber && + overlay.endingTimeSlotElementNumber >= + innerOverlay.startingTimeSlotElementNumber + ) { + return true; + } + return false; + }); + + if (firstSkippedOverElement) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotOverlapErrorMessage"), + ); + let elementBeforeFirstSkippedOverElement = + this.$refs.calendarSelectorContainer.querySelector( + `div[data-number='${firstSkippedOverElement.startingTimeSlotElementNumber - 1}']`, + ); + if (!elementBeforeFirstSkippedOverElement) return; + + let elementBeforeFirstSkippedOverElementNumber = parseInt( + elementBeforeFirstSkippedOverElement.getAttribute("data-number"), + ); + + const rect = + elementBeforeFirstSkippedOverElement.getBoundingClientRect(); + const overlayElementRect = overlayElement.getBoundingClientRect(); + const deltaY = rect.bottom - overlayElementRect.bottom; + + overlayElement.style.height = + parseInt(overlayElement.style.height) + deltaY + "px"; + + this.overlays = this.overlays.map((innerOverlay) => { + if (innerOverlay.id === overlay.id) { + innerOverlay.endingTimeSlotElementNumber = + elementBeforeFirstSkippedOverElementNumber; + innerOverlay.endingTimeSlot = + this.timeSlotsInDay[ + elementBeforeFirstSkippedOverElementNumber % + this.timeSlotsInDay.length + ]; + } + return innerOverlay; + }); + + this.oldMousePosition.x = null; + this.oldMousePosition.y = null; + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === draggedItemId) { + const timeSlot = + this.timeSlotsInDay[ + elementBeforeFirstSkippedOverElementNumber % + this.timeSlotsInDay.length + ]; + overlay.endingTimeSlotElementNumber = + elementBeforeFirstSkippedOverElementNumber; + overlay.endingTimeSlot = timeSlot; + + overlay.weekday = gridLineNumber + 1; + return overlay; + } + return overlay; + }); + } + + draggedItem.classList.remove("fhc-pointer-events-none"); + draggedItem.classList.add("fhc-pointer-events-all"); + }, + handleMouseDownOnResizer(event, resizerId, overlayId) { + if (this.$props.isPreviewMode) return; + + let isLeftMouseButton = event.buttons === 1; + if (!isLeftMouseButton) return; + + this.isTimeElementResizingInProgress = true; + + this.currentResizer = { + id: resizerId, + overlayId: overlayId, + }; + + this.$refs.calendarSelectorContainer + .querySelector(`#${overlayId}`) + .setAttribute("draggable", "false"); + }, + handleMouseMoveOnResizerColumn(event) { + if (this.$props.isPreviewMode) return; + + if (!this.isTimeElementResizingInProgress) { + this.$refs.calendarSelectorContainer + .querySelector(".fhc-calendar-base-grid-line") + .classList.add("fhc-pointer-events-none"); + return; + } + + let overlayElement1 = this.$refs.calendarSelectorContainer.querySelector( + `#${this.currentResizer.overlayId}`, + ); + let overlayTop = parseInt(overlayElement1.getBoundingClientRect().top); + let mouseY = event.clientY; + + if (mouseY < overlayTop + 5) { + this.isTimeElementResizingInProgress = false; + + this.oldMousePosition.x = null; + this.oldMousePosition.y = null; + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotMinimumSizeErrorMessage"), + ); + this.handleMouseUpOnResizerColumn(event); + + let currentStartingTimeSlotElement = + this.$refs.calendarSelectorContainer.querySelector( + `div[data-number='${ + this.overlays.find( + (overlay) => overlay.id === this.currentResizer.overlayId, + ).startingTimeSlotElementNumber + }']`, + ); + if (!currentStartingTimeSlotElement) return; + + const rect = currentStartingTimeSlotElement.getBoundingClientRect(); + const resizerRect = overlayElement1.getBoundingClientRect(); + const deltaY = rect.bottom - resizerRect.bottom; + + overlayElement1.style.height = + parseInt(overlayElement1.style.height) + deltaY + "px"; + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === this.currentResizer.overlayId) { + overlay.endingTimeSlotElementNumber = parseInt( + currentStartingTimeSlotElement.getAttribute("data-number"), + ); + overlay.endingTimeSlot = + this.timeSlotsInDay[ + parseInt( + currentStartingTimeSlotElement.getAttribute("data-number"), + ) % this.timeSlotsInDay.length + ]; + return overlay; + } + return overlay; + }); + + this.currentResizer = { + id: null, + overlayId: null, + }; + return; + } + + this.$refs.calendarSelectorContainer + .querySelector(".fhc-calendar-base-grid-line") + .classList.remove("fhc-pointer-events-none"); + + if (!this.oldMousePosition.x || !this.oldMousePosition.y) { + this.oldMousePosition.x = event.clientX; + this.oldMousePosition.y = event.clientY; + return; + } + + const resizerElement = this.$refs.calendarSelectorContainer.querySelector( + `#${this.currentResizer.id}`, + ); + const overlayElement = this.$refs.calendarSelectorContainer.querySelector( + `#${this.currentResizer.overlayId}`, + ); + + if (!resizerElement || !overlayElement) return; + + const newMousePosition = { + x: event.clientX, + y: event.clientY, + }; + + const deltaY = newMousePosition.y - this.oldMousePosition.y; + if (deltaY > 0) { + resizerElement.style.bottom = + parseInt(resizerElement.style.bottom || 0) - deltaY + "px"; + overlayElement.style.height = + parseInt(overlayElement.style.height) + deltaY + "px"; + } else { + resizerElement.style.bottom = + parseInt(resizerElement.style.bottom || 0) - deltaY + "px"; + overlayElement.style.height = + parseInt(overlayElement.style.height) + deltaY + "px"; + } + + this.oldMousePosition.x = newMousePosition.x; + this.oldMousePosition.y = newMousePosition.y; + }, + handleMouseUpOnResizerColumn(event) { + if (this.$props.isPreviewMode) return; + + if (!this.isTimeElementResizingInProgress) { + this.$refs.calendarSelectorContainer + .querySelector(".fhc-calendar-base-grid-line") + .classList.add("fhc-pointer-events-none"); + return; + } + + let isLeftMouseButton = event.buttons === 0; + if (!isLeftMouseButton) return; + + this.isTimeElementResizingInProgress = false; + this.$refs.calendarSelectorContainer + .querySelector(".fhc-calendar-base-grid-line") + .classList.add("fhc-pointer-events-none"); + + const resizerElement = this.$refs.calendarSelectorContainer.querySelector( + `#${this.currentResizer.id}`, + ); + const overlayElement = this.$refs.calendarSelectorContainer.querySelector( + `#${this.currentResizer.overlayId}`, + ); + + if (!resizerElement || !overlayElement) { + this.oldMousePosition.x = null; + this.oldMousePosition.y = null; + return; + } + + let editedOverlay = this.overlays.find( + (overlay) => overlay.id === this.currentResizer.overlayId, + ); + + const partBodies = this.$refs.calendarSelectorContainer.querySelectorAll( + "div[data-weekday='" + editedOverlay.weekday + "']", + ); + let closestPartBody = null; + let closestDistance = Infinity; + + partBodies.forEach((partBody) => { + const rect = partBody.getBoundingClientRect(); + const distance = Math.abs(event.clientY - rect.top - rect.height / 2); + + if (distance < closestDistance) { + closestDistance = distance; + closestPartBody = partBody; + } + }); + + if (closestPartBody) { + const closestPartBodyNumber = parseInt( + closestPartBody.getAttribute("data-number"), + ); + + let firstSkippedOverElement = this.overlays.find((overlay) => { + if (overlay.id === this.currentResizer.overlayId) return false; + + if ( + editedOverlay.startingTimeSlotElementNumber <= + overlay.startingTimeSlotElementNumber && + closestPartBodyNumber >= overlay.startingTimeSlotElementNumber + ) { + return true; + } + return false; + }); + + if (firstSkippedOverElement) { + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotOverlapErrorMessage"), + ); + let elementBeforeFirstSkippedOverElement = + this.$refs.calendarSelectorContainer.querySelector( + `div[data-number='${firstSkippedOverElement.startingTimeSlotElementNumber - 1}']`, + ); + if (!elementBeforeFirstSkippedOverElement) return; + + let elementBeforeFirstSkippedOverElementNumber = parseInt( + elementBeforeFirstSkippedOverElement.getAttribute("data-number"), + ); + + const rect = + elementBeforeFirstSkippedOverElement.getBoundingClientRect(); + const resizerRect = resizerElement.getBoundingClientRect(); + const deltaY = rect.bottom - resizerRect.bottom; + + overlayElement.style.height = + parseInt(overlayElement.style.height) + deltaY + "px"; + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === this.currentResizer.overlayId) { + overlay.endingTimeSlotElementNumber = + elementBeforeFirstSkippedOverElementNumber; + overlay.endingTimeSlot = + this.timeSlotsInDay[ + elementBeforeFirstSkippedOverElementNumber % + this.timeSlotsInDay.length + ]; + } + return overlay; + }); + + this.oldMousePosition.x = null; + this.oldMousePosition.y = null; + + return; + } + const rect = closestPartBody.getBoundingClientRect(); + const resizerRect = resizerElement.getBoundingClientRect(); + const deltaY = rect.bottom - resizerRect.bottom; + + overlayElement.style.height = + parseInt(overlayElement.style.height) + deltaY + "px"; + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === this.currentResizer.overlayId) { + const closestPartBodyTimeSlot = + this.timeSlotsInDay[ + closestPartBodyNumber % this.timeSlotsInDay.length + ]; + + overlay.endingTimeSlotElementNumber = closestPartBodyNumber; + overlay.endingTimeSlot = closestPartBodyTimeSlot; + return overlay; + } + return overlay; + }); + } + + this.oldMousePosition.x = null; + this.oldMousePosition.y = null; + + this.$refs.calendarSelectorContainer + .querySelector(`#${this.currentResizer.overlayId}`) + .setAttribute("draggable", "true"); + + this.currentResizer = { + id: null, + overlayId: null, + }; + }, + handleMouseOverOnOverlay(overlayId) { + if (this.$props.isPreviewMode) return; + + if (!this.isTimeElementCreationInProgress) return; + + let hitOverlay = this.overlays.find( + (overlay) => overlay.id === overlayId, + ); + + let startingTimeSlotElementNumber = + this.currentFirstSelectedElementNumber; + let hitOverlayStartingTimeSlotElementNumber = + hitOverlay.startingTimeSlotElementNumber; + + if ( + startingTimeSlotElementNumber < hitOverlayStartingTimeSlotElementNumber + ) { + this.currentLastSelectedElementNumber = + hitOverlayStartingTimeSlotElementNumber - 1; + } else { + this.currentFirstSelectedElementNumber = + hitOverlayStartingTimeSlotElementNumber + 1; + } + this.createOverlay(); + this.isTimeElementCreationInProgress = false; + this.$refs.calendarSelectorContainer + .querySelectorAll("div[class*='part-body']") + .forEach((child) => { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + }); + + this.currentFirstSelectedElementNumber = null; + this.currentLastSelectedElementNumber = null; + }, + handleMouseLeaveOnCalendar(event) { + if (this.$props.isPreviewMode) return; + + if (this.isTimeElementCreationInProgress) { + this.isTimeElementCreationInProgress = false; + this.$refs.calendarSelectorContainer + .querySelectorAll("div[class*='part-body']") + .forEach((child) => { + child.style.backgroundColor = this.defaultTimeSlotLabelColor; + }); + this.currentFirstSelectedElementNumber = null; + this.currentLastSelectedElementNumber = null; + } + + if (this.isTimeElementResizingInProgress) { + let overlayElement1 = + this.$refs.calendarSelectorContainer.querySelector( + `#${this.currentResizer.overlayId}`, + ); + + this.isTimeElementResizingInProgress = false; + this.$refs.calendarSelectorContainer + .querySelector(".fhc-calendar-base-grid-line") + .classList.add("fhc-pointer-events-none"); + + this.isTimeElementResizingInProgress = false; + + this.oldMousePosition.x = null; + this.oldMousePosition.y = null; + this.$fhcAlert.alertError( + this.$p.t("ui", "classTimeSlotResizeOutOfScopeErrorMessage"), + 1000, + ); + this.handleMouseUpOnResizerColumn(event); + + let previousEndingTimeSlotElement = + this.$refs.calendarSelectorContainer.querySelector( + `div[data-number='${ + this.overlays.find( + (overlay) => overlay.id === this.currentResizer.overlayId, + ).endingTimeSlotElementNumber + }']`, + ); + if (!previousEndingTimeSlotElement) return; + + const rect = previousEndingTimeSlotElement.getBoundingClientRect(); + const resizerRect = overlayElement1.getBoundingClientRect(); + const deltaY = rect.bottom - resizerRect.bottom; + + overlayElement1.style.height = + parseInt(overlayElement1.style.height) + deltaY + "px"; + + this.overlays = this.overlays.map((overlay) => { + if (overlay.id === this.currentResizer.overlayId) { + overlay.endingTimeSlotElementNumber = parseInt( + previousEndingTimeSlotElement.getAttribute("data-number"), + ); + overlay.endingTimeSlot = + this.timeSlotsInDay[ + parseInt( + previousEndingTimeSlotElement.getAttribute("data-number"), + ) % this.timeSlotsInDay.length + ]; + return overlay; + } + return overlay; + }); + + this.$refs.calendarSelectorContainer + .querySelector(".fhc-calendar-base-grid-line") + .classList.remove("fhc-pointer-events-none"); + + this.$refs.calendarSelectorContainer + .querySelector(`#${this.currentResizer.overlayId}`) + .setAttribute("draggable", "true"); + + this.currentResizer = { + id: null, + overlayId: null, + }; + + return; + } + + this.$refs.calendarSelectorContainer + .querySelectorAll("div[id^='overlay-']") + .forEach((element) => { + element.classList.remove("fhc-pointer-events-none"); + element.classList.add("fhc-pointer-events-all"); + }); + }, + isOverlayMinimallySized(overlay) { + if (!overlay) return false; + if ( + !overlay.startingTimeSlotElementNumber || + !overlay.endingTimeSlotElementNumber + ) { + return false; + } + + return ( + overlay.startingTimeSlotElementNumber === + overlay.endingTimeSlotElementNumber + ); + }, + }, + template: /*html*/ ` +
+
+
+
+
+
+
+
+ {{ day }} +
+
+
+
+
+
+
+
+
+ {{ timeSlot.split('-')[0] }}-{{ timeSlot.split('-')[1] }} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +

+ {{ getOverlayTimeSlotSpan("overlay-item-" + index) }} +

+
+ + + +
+
+ + + +
+
+
+
+
+ + {{ this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index)?.type }} + +

{{ getOverlayTimeSlotSpan("overlay-item-preview-" + index) }}

+
+
+
+
+ `, +}; diff --git a/public/js/components/ClassSchedule/ClassScheduleOverview.js b/public/js/components/ClassSchedule/ClassScheduleOverview.js index e27a63932..5628aed20 100644 --- a/public/js/components/ClassSchedule/ClassScheduleOverview.js +++ b/public/js/components/ClassSchedule/ClassScheduleOverview.js @@ -232,7 +232,7 @@ export default { this.phrasesLoaded = true; }); }, - template: ` + template: /* html */`

{{ $p.t("ui", "classScheduleOverviewHeading") }}

diff --git a/public/js/components/ClassSchedule/ClassScheduleTypeModal.js b/public/js/components/ClassSchedule/ClassScheduleTypeModal.js index 31cec71da..15cb83c00 100644 --- a/public/js/components/ClassSchedule/ClassScheduleTypeModal.js +++ b/public/js/components/ClassSchedule/ClassScheduleTypeModal.js @@ -1,4 +1,3 @@ -import { CoreFilterCmpt } from "../filter/Filter.js"; import ApiClassSchedule from "../../../js/api/factory/classSchedule.js"; import BsModal from "../Bootstrap/Modal.js"; @@ -6,7 +5,7 @@ import CoreForm from "../Form/Form.js"; import FormInput from "../Form/Input.js"; export default { - name: "ClassScheduleValidityPeriodForm", + name: "ClassScheduleTypeModal", components: { BsModal, CoreForm, @@ -43,6 +42,7 @@ export default { { lang: "de", value: "" }, { lang: "en", value: "" }, ], + backgroundColor: "#ffffff", }, classScheduleTypes: [], }; @@ -80,6 +80,7 @@ export default { isActive: this.classTimeSlotTypeFormData.isActive, shortCode: this.classTimeSlotTypeFormData.shortCode, descriptions: this.classTimeSlotTypeFormData.descriptions, + backgroundColor: this.classTimeSlotTypeFormData.backgroundColor, }), ) .then((response) => { @@ -102,6 +103,7 @@ export default { value: desc.value, }), ), + backgroundColor: classScheduleType.hintergrundfarbe || "#ffffff", }; }, updateClassTimeSlotType() { @@ -112,6 +114,7 @@ export default { { isActive: this.classTimeSlotTypeFormData.isActive, descriptions: this.classTimeSlotTypeFormData.descriptions, + backgroundColor: this.classTimeSlotTypeFormData.backgroundColor, }, ), ) @@ -163,13 +166,14 @@ export default { { lang: "de", value: "" }, { lang: "en", value: "" }, ], + backgroundColor: "#ffffff", }; }, }, async created() { await this.getAllClassScheduleTypes(); }, - template: ` + template: /* html */`