add prefiltering on validity period form component

This commit is contained in:
Ivymaster
2026-04-24 14:08:11 +02:00
parent 869a7e3335
commit 7307c1fa44
20 changed files with 1181 additions and 282 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/prestudent/(:num)'] = 'a
$route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/person/(:num)'] = 'api/frontend/v1/stv/Students/getPerson/$1/$2';
$route['lehre/ClassSchedule/(:any)/(:any)'] = 'lehre/ClassSchedule/index';
$route['lehre/ClassSchedule/.*'] = 'lehre/ClassSchedule/index';
// load routes from extensions, also look for environment-specific configs
$subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions'];
@@ -27,6 +27,7 @@ class ClassScheduleApi extends FHCAPI_Controller
{
parent::__construct([
'getAllClassTimeValidityPeriods'=> array('lehre/unterrichtszeiten_gk:r'),
'getAllClassTimeValidityPeriodsPerOrganizationalUnit' => array('lehre/unterrichtszeiten_gk:r'),
'getClassTimeValidityPeriod' => array('lehre/unterrichtszeiten_gk:r'),
'createClassTimeSlotValidityPeriod' => array('lehre/unterrichtszeiten_gk:rw'),
'updateClassTimeSlotValidityPeriod' => array('lehre/unterrichtszeiten_gk:rw'),
@@ -60,14 +61,40 @@ class ClassScheduleApi extends FHCAPI_Controller
public function getAllClassTimeValidityPeriods()
{
$this->ClassTimeSlotValidityPeriodModel->addSelect(
'lehre.tbl_unterrichtszeiten_gueltigkeit.*,' .
'public.tbl_organisationseinheit.bezeichnung as organisationseinheit_bezeichnung,' .
'public.tbl_organisationseinheit.organisationseinheittyp_kurzbz as organisationseinheit_organisationseinheittyp_kurzbz,' .
'lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung,' .
'lehre.tbl_unterrichtszeiten_typ.bezeichnung_mehrsprachig as unterrichtszeiten_typ_bezeichnung_mehrsprachig, '
);
$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->addJoin('lehre.tbl_unterrichtszeiten_typ', 'lehre.tbl_unterrichtszeiten_typ.unterrichtszeitentyp_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.unterrichtszeitentyp_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);
$this->terminateWithSuccess($class_time_slot_validity_period_res);
}
public function getAllClassTimeValidityPeriodsPerOrganizationalUnit($organizationUnitId)
{
$this->ClassTimeSlotValidityPeriodModel->addSelect(
'lehre.tbl_unterrichtszeiten_gueltigkeit.*,' .
'public.tbl_organisationseinheit.bezeichnung as organisationseinheit_bezeichnung,' .
'public.tbl_organisationseinheit.organisationseinheittyp_kurzbz as organisationseinheit_organisationseinheittyp_kurzbz,' .
'lehre.tbl_studienplan.bezeichnung as studienplan_bezeichnung,' .
'lehre.tbl_unterrichtszeiten_typ.bezeichnung_mehrsprachig as unterrichtszeiten_typ_bezeichnung_mehrsprachig, '
);
$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->addJoin('lehre.tbl_unterrichtszeiten_typ', 'lehre.tbl_unterrichtszeiten_typ.unterrichtszeitentyp_kurzbz=lehre.tbl_unterrichtszeiten_gueltigkeit.unterrichtszeitentyp_kurzbz', 'LEFT');
$this->ClassTimeSlotValidityPeriodModel->addOrder('gueltig_von', 'DESC');
$class_time_slot_validity_period_res = $this->ClassTimeSlotValidityPeriodModel->loadWhere(['lehre.tbl_unterrichtszeiten_gueltigkeit.oe_kurzbz' => $organizationUnitId]);
$class_time_slot_validity_period_res = $this->getDataOrTerminateWithError($class_time_slot_validity_period_res);
$this->terminateWithSuccess($class_time_slot_validity_period_res);
}
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');
@@ -21,6 +21,7 @@ class Studienplan extends FHCAPI_Controller
// TODO(chris): access!
parent::__construct([
'getAllStudyPlans' => self::PERM_LOGGED,
'getStudyPlansByOrganizationalUnitAndSemesterDates' => self::PERM_LOGGED,
'getBySemester' => self::PERM_LOGGED
]);
}
@@ -33,6 +34,26 @@ class Studienplan extends FHCAPI_Controller
$this->terminateWithSuccess($studien_plan_result);
}
public function getStudyPlansByOrganizationalUnitAndSemesterDates($organizationalUnitShortCode)
{
$this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('organisation/Studienordnung_model', 'StudienordnungModel');
$this->load->model('organisation/Studienplan_model', 'StudienplanModel');
$startDate = date('Y-m-d', strtotime($this->input->get('filter[startDate]')));
$endDate = date('Y-m-d', strtotime($this->input->get('filter[endDate]')));
if (!$startDate || !$endDate) {
return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Start- oder Enddatum']), self::ERROR_TYPE_GENERAL);
}
$studyPlansResponse = $this->StudienplanModel->getStudyPlansForOrganizationalUnitAndDatesQueryResponse($organizationalUnitShortCode, $startDate, $endDate);
if (isError($studyPlansResponse)) $this->terminateWithError(getError($studyPlansResponse), self::ERROR_TYPE_DB);
if (!hasData($studyPlansResponse)) return $this->terminateWithSuccess(null);
return $this->terminateWithSuccess($this->getDataOrTerminateWithError($studyPlansResponse));
}
public function getBySemester()
{
$this->load->model('organisation/Studienplan_model', 'StudienplanModel');
@@ -26,7 +26,9 @@ class Studiensemester extends FHCAPI_Controller
'getAll' => self::PERM_LOGGED,
'getAktNext' => self::PERM_LOGGED,
'getStudienjahrByStudiensemester' => self::PERM_LOGGED,
'getAllStudiensemesterAndAktOrNext' => self::PERM_LOGGED
'getAllStudiensemesterAndAktOrNext' => self::PERM_LOGGED,
'getStudySemestersByOrganizationalUnitAndDates' => self::PERM_LOGGED,
'getStudySemestersByStudyPlanAndDates' => self::PERM_LOGGED,
)
);
// Load model StudiensemesterModel
@@ -166,4 +168,51 @@ class Studiensemester extends FHCAPI_Controller
$this->terminateWithSuccess(array($studiensemester, $aktuell));
}
public function getStudySemestersByOrganizationalUnitAndDates($organizationalUnitShortCode)
{
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('organisation/Studienordnung_model', 'StudienordnungModel');
$this->load->model('organisation/Studienplan_model', 'StudienplanModel');
$startDate = date('Y-m-d', strtotime($this->input->get('filter[startDate]')));
$endDate = date('Y-m-d', strtotime($this->input->get('filter[endDate]')));
if (!$startDate || !$endDate) {
return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Start- oder Enddatum']), self::ERROR_TYPE_GENERAL);
}
$studyPlansResponse = $this->StudienplanModel->getStudyPlansForOrganizationalUnitQueryResponse($organizationalUnitShortCode);
if (isError($studyPlansResponse)) $this->terminateWithError(getError($studyPlansResponse), self::ERROR_TYPE_DB);
if (!hasData($studyPlansResponse)) return $this->terminateWithSuccess(null);
$studyPlans = $this->getDataOrTerminateWithError($studyPlansResponse);
$studyPlansIds = array_map(function ($studyPlan) { return $studyPlan->studienplan_id; }, $studyPlans);
$studySemestersResponse = $this->StudiensemesterModel->getStudySemestersByStudyPlansAndDatesQueryResponse($studyPlansIds, $startDate, $endDate);
if (isError($studySemestersResponse)) $this->terminateWithError(getError($studySemestersResponse), self::ERROR_TYPE_DB);
if (!hasData($studySemestersResponse)) return $this->terminateWithSuccess([]);
$studySemesters = $this->getDataOrTerminateWithError($studySemestersResponse);
return $this->terminateWithSuccess($studySemesters);
}
public function getStudySemestersByStudyPlanAndDates()
{
$studyPlansId = $this->input->get('filter[studyPlanId]');
$startDate = date('Y-m-d', strtotime($this->input->get('filter[startDate]')));
$endDate = date('Y-m-d', strtotime($this->input->get('filter[endDate]')));
if (!$startDate || !$endDate) {
return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Start- oder Enddatum']), self::ERROR_TYPE_GENERAL);
}
$studySemestersResponse = $this->StudiensemesterModel->getStudySemestersByStudyPlansAndDatesQueryResponse([$studyPlansId], $startDate, $endDate);
if (isError($studySemestersResponse)) $this->terminateWithError(getError($studySemestersResponse), self::ERROR_TYPE_DB);
if (!hasData($studySemestersResponse)) return $this->terminateWithSuccess([]);
$studySemesters = $this->getDataOrTerminateWithError($studySemestersResponse);
return $this->terminateWithSuccess($studySemesters);
}
}
@@ -168,5 +168,30 @@ class Studienplan_model extends DB_Model
return $this->loadWhere([
'person_id' => $person_id
]);
}
public function getStudyPlansForOrganizationalUnitQueryResponse($organizationalUnitShortCode)
{
$query = "SELECT sp.* FROM lehre.tbl_studienplan sp
JOIN lehre.tbl_studienordnung so ON sp.studienordnung_id = so.studienordnung_id
JOIN public.tbl_studiengang sg ON so.studiengang_kz = sg.studiengang_kz
WHERE sg.oe_kurzbz = ?";
return $this->execReadOnlyQuery($query, array($organizationalUnitShortCode));
}
public function getStudyPlansForOrganizationalUnitAndDatesQueryResponse($organizationalUnitShortCode, $startDate, $endDate)
{
$query = "SELECT DISTINCT sp.* FROM lehre.tbl_studienplan sp
JOIN lehre.tbl_studienordnung so ON sp.studienordnung_id = so.studienordnung_id
JOIN public.tbl_studiengang sg ON so.studiengang_kz = sg.studiengang_kz
AND sg.oe_kurzbz = ? AND sp.studienplan_id IN (
SELECT sps.studienplan_id
FROM lehre.tbl_studienplan_semester sps
JOIN public.tbl_studiensemester ss ON ss.studiensemester_kurzbz = sps.studiensemester_kurzbz
WHERE (ss.start >= ? AND ss.ende <= ?) OR (ss.start <= ? AND ss.ende >= ?)
)";
return $this->execReadOnlyQuery($query, array($organizationalUnitShortCode, $startDate, $endDate, $startDate, $endDate));
}
}
@@ -347,4 +347,24 @@ class Studiensemester_model extends DB_Model
$result = $this->load($studiensemester_kurzbz);
return hasData($result);
}
public function getStudySemestersByStudyPlansAndDatesQueryResponse($studyPlanIds, $fromDate, $toDate)
{
$subQuery = "(
SELECT ARRAY_AGG(DISTINCT sp.semester)
FROM lehre.tbl_studienplan_semester sp
WHERE sp.studienplan_id IN ?
AND sp.studiensemester_kurzbz = s.studiensemester_kurzbz
) AS semester_numbers";
$query = "SELECT DISTINCT s.*," . $subQuery . "
FROM public.tbl_studiensemester s
JOIN lehre.tbl_studienplan_semester sp ON sp.studiensemester_kurzbz = s.studiensemester_kurzbz
WHERE sp.studienplan_id IN ? AND (
(s.start >= ? AND s.ende <= ?) OR
(s.start <= ? AND s.ende >= ?)
)";
return $this->execQuery($query, array($studyPlanIds, $studyPlanIds, $fromDate, $toDate, $fromDate, $toDate));
}
}
+22 -2
View File
@@ -11,7 +11,7 @@
}
.fhc-resize-vertical {
cursor: ns-resize;
cursor: ns-resize;
}
.fhc-drag-handle:hover {
@@ -21,4 +21,24 @@
.fhc-w-fit {
width: fit-content;
}
}
.tabulator-group-level-0 {
background-color: #c6c6c6 !important;
display: flex;
align-items: center;
}
.tabulator-group-level-1 {
background-color: #f5f5f5 !important;
display: flex;
align-items: center;
}
.tabulator-cell {
background-color: transparent !important;
}
div[role="row"] {
background-color: white;
}
+6
View File
@@ -22,6 +22,12 @@ export default {
url: "/api/frontend/v1/ClassScheduleApi/getAllClassTimeValidityPeriods",
};
},
getAllClassTimeValidityPeriodsPerOrganizationalUnit(organizationUnitId) {
return {
method: "get",
url: `/api/frontend/v1/ClassScheduleApi/getAllClassTimeValidityPeriodsPerOrganizationalUnit/${organizationUnitId}`,
};
},
getClassTimeValidityPeriod(classTimeSlotValidityPeriodId) {
return {
method: "get",
+24 -9
View File
@@ -5,12 +5,27 @@ export default {
url: "api/frontend/v1/organisation/studienplan/getAllStudyPlans",
};
},
getStudienplaeneBySemester(studiengang_kz, studiensemester_kurzbz, ausbildungssemester, orgform_kurzbz)
{
return {
method: 'get',
url: 'api/frontend/v1/organisation/studienplan/getBySemester',
params: { studiengang_kz, studiensemester_kurzbz, ausbildungssemester, orgform_kurzbz },
};
}
}
getStudyPlansByOrganizationalUnitAndSemesterDates(organizationalUnitShortCode, startDate, endDate) {
return {
method: "get",
url: `api/frontend/v1/organisation/studienplan/getStudyPlansByOrganizationalUnitAndSemesterDates/${organizationalUnitShortCode}?filter[startDate]=${startDate}&filter[endDate]=${endDate}`,
};
},
getStudienplaeneBySemester(
studiengang_kz,
studiensemester_kurzbz,
ausbildungssemester,
orgform_kurzbz,
) {
return {
method: "get",
url: "api/frontend/v1/organisation/studienplan/getBySemester",
params: {
studiengang_kz,
studiensemester_kurzbz,
ausbildungssemester,
orgform_kurzbz,
},
};
},
};
+13 -1
View File
@@ -29,5 +29,17 @@ export default {
url: 'api/frontend/v1/organisation/studiensemester/getAll',
params: { order, start }
};
}
},
getStudySemestersByOrganizationalUnitAndDates(organizationalUnitShortCode, startDate, endDate) {
return {
method: "get",
url: `api/frontend/v1/organisation/studiensemester/getStudySemestersByOrganizationalUnitAndDates/${organizationalUnitShortCode}?filter[startDate]=${startDate}&filter[endDate]=${endDate}`,
};
},
getStudySemestersByStudyPlanAndDates(studyPlanId, startDate, endDate) {
return {
method: "get",
url: `api/frontend/v1/organisation/studiensemester/getStudySemestersByStudyPlanAndDates?filter[studyPlanId]=${studyPlanId}&filter[startDate]=${startDate}&filter[endDate]=${endDate}`,
};
},
};
+14
View File
@@ -17,6 +17,7 @@
import ClassScheduleOverview from "../../components/ClassSchedule/ClassScheduleOverview.js";
import ClassScheduleValidityPeriodOverview from "../../components/ClassSchedule/ClassScheduleValidityPeriodOverview.js";
import ClassScheduleOrgUnitGroupedValidityPeriodsOverview from "../../components/ClassSchedule/ClassScheduleOrgUnitGroupedValidityPeriodsOverview.js";
import FhcAlert from "../../plugins/FhcAlert.js";
import Phrasen from "../../plugins/Phrasen.js";
@@ -34,6 +35,18 @@ const router = VueRouter.createRouter({
path: `/${ciPath}/lehre/ClassSchedule`,
component: ClassScheduleOverview,
},
{
name: "validityPeriodsOverviewByOrganizationUnit",
path: `/${ciPath}/lehre/ClassSchedule/validityPeriods/groupedOverviewByOrganizationUnit/:organizationalUnitShortCode`,
component: ClassScheduleOrgUnitGroupedValidityPeriodsOverview,
params: true,
},
{
name: "validityPeriodsOverviewByOrganizationUnitAndStudyPlan",
path: `/${ciPath}/lehre/ClassSchedule/validityPeriods/groupedOverviewByOrganizationUnit/:organizationalUnitShortCode/studyPlan/:studyPlanId`,
component: ClassScheduleOrgUnitGroupedValidityPeriodsOverview,
params: true,
},
{
name: "validityPeriodOverview",
path: `/${ciPath}/lehre/ClassSchedule/validityPeriods/:classTimeSlotValidityPeriodId`,
@@ -47,6 +60,7 @@ const app = Vue.createApp({
components: {
ClassScheduleOverview,
ClassScheduleValidityPeriodOverview,
ClassScheduleOrgUnitGroupedValidityPeriodsOverview,
},
});
@@ -1,12 +1,8 @@
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,
@@ -30,91 +26,91 @@ export default {
emits: ["overlaysChanged"],
watch: {
editedOverlays: {
handler(newVal) {
setTimeout(() => {
this.overlays = [];
async handler(newVal) {
await this.$nextTick();
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,
};
this.overlays = [];
await this.$nextTick();
this.$refs.calendarContainer
.querySelectorAll("div[id^='overlay-']")
.forEach((element) => {
element.remove();
});
}, 500);
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,
};
});
},
deep: true,
immediate: true,
@@ -129,13 +125,13 @@ export default {
data() {
return {
daysInWeek: [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
this.$p.t("ui", "monday"),
this.$p.t("ui", "tuesday"),
this.$p.t("ui", "wednesday"),
this.$p.t("ui", "thursday"),
this.$p.t("ui", "friday"),
this.$p.t("ui", "saturday"),
this.$p.t("ui", "sunday"),
],
timeSlotsInDay: [
"08:00-08:45",
@@ -173,6 +169,8 @@ export default {
x: null,
y: null,
},
currentlyEditedOverlayId: null,
visiblePopover: null,
};
},
computed: {
@@ -209,15 +207,9 @@ export default {
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];
}
overlayElement = this.$refs.calendarSelectorContainer.querySelector(
"#overlays-container",
).children[0];
let firstSelectedChild =
this.$refs.calendarSelectorContainer.querySelector(
@@ -709,11 +701,15 @@ export default {
let dropzoneItem = event.target;
if (!dropzoneItem) return;
if (!dropzoneItem.getAttribute("data-time")) {
let dropzoneOverlay = this.overlays.find((overlay) => overlay.id === dropzoneItem.id);
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);
console.error(
"Could not find overlay for dropzone item with id " +
dropzoneItem.id,
);
return;
}
@@ -726,7 +722,7 @@ export default {
let startElementNumber = dropzoneOverlay.startingTimeSlotElementNumber;
let startElement = this.$refs.calendarSelectorContainer.querySelector(
`[data-number='${startElementNumber}']`
`[data-number='${startElementNumber}']`,
);
// get mouse position
@@ -742,9 +738,10 @@ export default {
// 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 + "']",
);
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;
@@ -756,7 +753,7 @@ export default {
});
if (!closestPartBody) return;
dropzoneItem = closestPartBody;
dropzoneItem = closestPartBody;
}
let newStartTimeSlot =
@@ -1351,6 +1348,101 @@ export default {
overlay.endingTimeSlotElementNumber
);
},
getOverlayClassScheduleTypeTitle(overlayId) {
let overlay = this.overlays.find((overlay) => overlay.id === overlayId);
if (!overlay) return "";
let typeDescriptions =
this.classTimeSlotTypes.find(
(type) => type.unterrichtszeitentyp_kurzbz === overlay.type,
)?.bezeichnung_mehrsprachig || "";
if (!typeDescriptions) return "";
return typeDescriptions[0].value || "";
},
handleChangeClassTimeSlotTypeForOverlay(newType) {
let classTimeSlotType = this.classTimeSlotTypes.find(
(type) => type.unterrichtszeitentyp_kurzbz === newType,
);
if (!classTimeSlotType) {
console.error(
"Could not find class time slot type for newType: " + newType,
);
return;
}
this.overlays = this.overlays.map((overlay) => {
if (overlay.id === this.currentlyEditedOverlayId) {
return {
...overlay,
type: classTimeSlotType.unterrichtszeitentyp_kurzbz,
hexColor: classTimeSlotType.hintergrundfarbe,
};
}
return overlay;
});
this.currentlyEditedOverlayId = null;
},
showOverlayClassTimeTypePopover(overlayId) {
let overlayElement = this.$refs.calendarSelectorContainer.querySelector(
`#${overlayId}`,
);
if (!overlayElement) return;
if (this.visiblePopover) {
this.visiblePopover.dispose();
this.visiblePopover = null;
return;
}
this.visiblePopover = new bootstrap.Popover(overlayElement, {
title: this.$p.t("ui", "classTimeSlotType"),
html: true,
content: this.$refs.classScheduleTypeSelectorContainer.innerHTML,
placement: "left",
});
this.visiblePopover.show();
setTimeout(() => {
document
.querySelectorAll(".class-schedule-type-selector-option")
.forEach((option) => {
option.addEventListener("click", (event) => {
let selectedTypeDescription = event.currentTarget.innerText;
let selectedType = this.classTimeSlotTypes.find((type) =>
type.bezeichnung_mehrsprachig.some(
(desc) => desc.value === selectedTypeDescription,
),
);
if (!selectedType) {
console.error(
"Could not find class time slot type for selected description: " +
selectedTypeDescription,
);
return;
}
this.overlays = this.overlays.map((overlay) => {
if (overlay.id === overlayId) {
return {
...overlay,
type: selectedType.unterrichtszeitentyp_kurzbz,
hexColor: selectedType.hintergrundfarbe,
};
}
return overlay;
});
this.visiblePopover.dispose();
this.visiblePopover = null;
});
});
}, 10);
},
},
unmounted() {
this.visiblePopover?.dispose();
this.visiblePopover = null;
},
template: /*html*/ `
<div ref="calendarSelectorContainer" >
@@ -1601,21 +1693,34 @@ export default {
</div>
</div>
</div>
<div v-if="!isPreviewMode" id="overlays-container" class='d-none'>
<div id="overlays-container" class='d-none'>
<div
v-for="(index) in 50"
v-draggable:copyLink.noimage="selectedDragObject"
@mousedown='overlaySelectionChanged($event, "overlay-item-" + index)'
@mouseover="handleMouseOverOnOverlay('overlay-item-' + index)"
:id="'overlay-item-' + index"
:class="{
'fhc-drag-handle': !$props.isPreviewMode,
}"
:style="{ backgroundColor: this.overlays.find(overlay => overlay.id === 'overlay-item-' + index)?.hexColor || this.defaultOverlayColor }"
class="d-none fhc-pointer-events-all flex-column justify-content-between align-items-center shadow rounded-1 fhc-drag-handle"
draggable='true'
:title="getOverlayClassScheduleTypeTitle('overlay-item-' + index)"
class="d-none fhc-pointer-events-all flex-column justify-content-between align-items-center shadow rounded-1"
:draggable='!$props.isPreviewMode ? "true" : "false"'
>
<div
class="d-flex flex-column justify-content-center align-items-center gap-1 p-2 overflow-scroll"
>
<a
v-if="!$props.isPreviewMode"
@mousedown.stop="showOverlayClassTimeTypePopover('overlay-item-' + index)"
:title="$p.t('ui', 'bearbeiten')"
class="position-absolute top-0 start-0 p-1 d-flex gap-1 fhc-cursor-pointer"
>
<i class="fa fa-edit text-primary fs-6"></i>
</a>
<a
v-if="!$props.isPreviewMode"
@mousedown.stop="deleteOverlay('overlay-item-' + index)"
:title="$p.t('global', 'loeschen')"
class="position-absolute top-0 end-0 p-1 d-flex gap-1 fhc-cursor-pointer"
@@ -1623,43 +1728,20 @@ export default {
<i class="fa fa-trash text-danger fs-6"></i>
</a>
<p
v-if="!isOverlayMinimallySized(this.overlays.find(overlay => overlay.id === 'overlay-item-' + index))"
:style="'color: ' + this.overlayTextColor"
class="bg-transparent rounded-1 py-0 m-0 "
>
{{ getOverlayTimeSlotSpan("overlay-item-" + index) }}
</p>
<div v-if="this.overlays.length">
<form-input
:modelValue="this.overlays.find(overlay => overlay.id === 'overlay-item-' + index)?.type"
@mousedown.stop
@change="(event) => {
let overlay = this.overlays.find(overlay => overlay.id === 'overlay-item-' + index);
if (!overlay) return;
overlay.type = event.target.value;
overlay.hexColor = this.classTimeSlotTypes.find(type => type.unterrichtszeitentyp_kurzbz === event.target.value)?.hintergrundfarbe;
}"
:class="{
'w-75': isOverlayMinimallySized(this.overlays.find(overlay => overlay.id === 'overlay-item-' + index)),
}"
type="select"
name="classTimeSlotType"
class="p-0 m-0 fs-6"
>
<option
v-for="type in $props.classTimeSlotTypes"
:key="type.unterrichtszeitentyp_kurzbz"
:value="type.unterrichtszeitentyp_kurzbz"
selected
>
{{type.unterrichtszeitentyp_kurzbz}}
</option>
</form-input>
</div>
<span
v-if="!isOverlayMinimallySized(this.overlays.find(overlay => overlay.id === 'overlay-item-' + index))"
class="badge badge-pill bg-light text-dark"
>
{{ this.getOverlayClassScheduleTypeTitle('overlay-item-' + index) }}
</span>
</div>
<span
<span
v-if="!$props.isPreviewMode"
@mousedown.stop="handleMouseDownOnResizer($event, 'overlay-item-resizer-' + index, 'overlay-item-' + index)"
:id="'overlay-item-resizer-' + index"
:class="{
@@ -1671,34 +1753,17 @@ export default {
</span>
</div>
</div>
<div v-if="isPreviewMode" id="overlays-container-preview" class='d-none'>
<div
v-for="(index) in 50"
:id="'overlay-item-preview-' + index"
:style="{ backgroundColor: this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index)?.hexColor || this.defaultOverlayColor }"
:class="isOverlayMinimallySized(this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index)) ? 'justify-content-center' : 'justify-content-between'"
class="d-flex flex-column align-items-center shadow rounded-1 "
<div ref="classScheduleTypeSelectorContainer" class="d-none">
<div class='d-flex flex-column gap-2'>
<span v-for="(type, index) in classTimeSlotTypes"
:key="index"
:data-type="type.unterrichtszeitentyp_kurzbz"
class="btn btn-sm btn-outline-dark class-schedule-type-selector-option"
>
<div
:class="{
'p-2': !isOverlayMinimallySized(this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index)),
}"
class="d-flex flex-column justify-content-center align-items-center gap-0"
>
<span
v-if="this.$props.isPreviewMode"
class="badge badge-pill bg-light text-dark"
>
{{ this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index)?.type }}
</span>
<p
v-if="!isOverlayMinimallySized(this.overlays.find(overlay => overlay.id === 'overlay-item-preview-' + index))"
:style="'color: ' + this.overlayTextColor"
class="bg-transparent rounded-1 py-0 m-0 "
>{{ getOverlayTimeSlotSpan("overlay-item-preview-" + index) }}</p>
</div>
{{ type.bezeichnung_mehrsprachig[0].value }}
</span>
</div>
</div>
</div>
</div>
`,
};
@@ -0,0 +1,115 @@
import ApiClassSchedule from "../../../js/api/factory/classSchedule.js";
import ClassScheduleValidityPeriodPreview from "./ClassScheduleValidityPeriodPreview.js";
export default {
name: "ClassScheduleOrgUnitGroupedValidityPeriodsOverview",
components: {
ClassScheduleValidityPeriodPreview,
},
data: () => {
return {
organizationalUnitShortCode: null,
studyPlanId: null,
classScheduleValidityPeriods: [],
classScheduleTimeSlots: [],
classTimeSlotTypes: [],
};
},
computed: {},
methods: {},
async created() {
this.organizationalUnitShortCode = this.$route.params.organizationalUnitShortCode;
this.studyPlanId = parseInt(this.$route.params.studyPlanId);
let getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse =
await this.$api.call(
ApiClassSchedule.getAllClassTimeValidityPeriodsPerOrganizationalUnit(
this.organizationalUnitShortCode,
),
);
if (
getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse.meta
.status === "success"
) {
this.classScheduleValidityPeriods =
getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse.data.filter(
(period) => {
if (!this.studyPlanId) return true;
return period.studienplan_id === this.studyPlanId;
}
);
} else {
console.error(
"Error fetching class time slot validity periods:",
getAllClassTimeValidityPeriodsPerOrganizationalUnitResponse.meta
.message,
);
}
for (let validityPeriod of this.classScheduleValidityPeriods) {
if (this.studyPlanId && validityPeriod.studienplan_id !== this.studyPlanId) {
continue;
}
let getClassTimeSlotsForValidityPeriodResponse = await this.$api.call(
ApiClassSchedule.getClassTimeSlotsForValidityPeriod(
validityPeriod.unterrichtszeitengueltigkeit_id,
),
);
if (
getClassTimeSlotsForValidityPeriodResponse.meta.status === "success"
) {
this.classScheduleTimeSlots = [
...this.classScheduleTimeSlots,
...getClassTimeSlotsForValidityPeriodResponse.data,
];
} else {
console.error(
`Error fetching class time slots for validity period ${validityPeriod.unterrichtszeitengueltigkeit_id}:`,
getClassTimeSlotsForValidityPeriodResponse.meta.message,
);
}
}
let getAllClassTimeSlotTypesResponse = await this.$api.call(
ApiClassSchedule.getAllClassScheduleTypes("filter[aktiv]=true"),
);
if (getAllClassTimeSlotTypesResponse.meta.status === "success") {
this.classTimeSlotTypes = getAllClassTimeSlotTypesResponse.data.map(
(type) => {
let descriptions = [];
for (let item of type.bezeichnung_mehrsprachig) {
let [lang, value] = item.split(":");
descriptions.push({ lang, value });
}
return {
...type,
bezeichnung_mehrsprachig: descriptions,
};
},
);
} else {
console.error(
"Error fetching class time slot types:",
getAllClassTimeSlotTypesResponse.meta.message,
);
}
},
template: /* html */ `
<div class="container mt-4">
<div class="my-2">
<h2 v-if="!this.studyPlanId">{{ $p.t("ui", "classScheduleOrgUnitGroupedValidityPeriodsOverviewTitle") }}</h2>
<h2 v-else>{{ $p.t("ui", "classScheduleStudyPlanGroupedValidityPeriodsOverviewTitle") }}</h2>
</div>
<class-schedule-validity-period-preview
v-for="validityPeriod in classScheduleValidityPeriods"
:key="validityPeriod.unterrichtszeitengueltigkeit"
:class-schedule-validity-period="validityPeriod"
:class-time-slots="classScheduleTimeSlots.filter(slot => slot.unterrichtszeitengueltigkeit_id === validityPeriod.unterrichtszeitengueltigkeit_id)"
:class-time-slot-types="classTimeSlotTypes"
class="shadow"
/>
</div>
`,
};
@@ -38,34 +38,48 @@ export default {
tabulatorOptions() {
const options = {
ajaxURL: "dummy",
ajaxRequestFunc: () =>
this.$api.call(ApiClassSchedule.getAllClassTimeValidityPeriods()),
ajaxResponse: (url, params, response) => response.data,
ajaxRequestFunc: async () =>
await this.getParsedClassTimeSlotValidityPeriodData(),
ajaxResponse: (url, params, response) => response,
persistenceID: "core_class_schedule_validity_periods",
selectableRows: true,
columns: [
{
title: "gueltig von",
field: "gueltig_von",
title: this.$p.t("ui", "zeitraum"),
formatter: (cell, formatterParams, onRendered) => {
const data = cell.getData();
const validFrom = new Date(data.gueltig_von).toLocaleDateString(
"de-AT",
{
day: "2-digit",
month: "2-digit",
year: "numeric",
},
);
data.gueltig_von;
const validTo = new Date(data.gueltig_bis).toLocaleDateString(
"de-AT",
{
day: "2-digit",
month: "2-digit",
year: "numeric",
},
);
return `${validFrom ? validFrom : "?"} - ${validTo ? validTo : "?"}`;
},
},
{
title: this.$p.t("global", "typ"),
field: "unterrichtszeiten_typ_bezeichnung_mehrsprachig",
width: 150,
},
{ title: "gueltig bis", field: "gueltig_bis", width: 150 },
{ title: "orgform kurzbz", field: "orgform_kurzbz", width: 150 },
{
title: "ausbildungssemester",
title: this.$p.t("lehre", "sem"),
field: "ausbildungssemester",
width: 150,
},
{
title: "typ",
field: "unterrichtszeitentyp_kurzbz",
},
{
field: "unterrichtszeitengueltigkeit_id",
visible: false,
},
{
title: "Aktionen",
title: this.$p.t("global", "actions"),
field: "actions",
minWidth: 150,
maxWidth: 150,
@@ -135,38 +149,102 @@ export default {
frozen: true,
},
],
groupBy: [
"organisationseinheit_bezeichnung_extended",
"studienplan_bezeichnung",
],
groupHeader: [
function (value, count, data) {
let container = document.createElement("span");
container.className =
"d-flex align-items-center justify-content-between";
container.style.display = "inline-block";
container.style.width = "100%";
let label = document.createElement("span");
label.textContent =
value + " (" + count + " item" + (count > 1 ? "s" : "") + ")";
container.append(label);
let button = document.createElement("button");
button.className =
"btn btn-sm btn-outline-secondary fhc-btn-for-org-unit-grouping";
button.style.marginLeft = "10px";
button.innerHTML = '<i class="fa fa-eye"></i>';
button.dataset.organizationalUnitShortCode = data[0].oe_kurzbz;
container.append(button);
return container;
},
function (value, count, data) {
let container = document.createElement("span");
container.className =
"d-flex align-items-center justify-content-between";
container.style.display = "inline-block";
container.style.width = "100%";
let label = document.createElement("span");
label.textContent =
value + " (" + count + " item" + (count > 1 ? "s" : "") + ")";
container.append(label);
let button = document.createElement("button");
button.className =
"btn btn-sm btn-outline-secondary fhc-btn-for-org-unit-and-study-plan-grouping";
button.style.marginLeft = "10px";
button.innerHTML = '<i class="fa fa-eye"></i>';
button.title = 22;
button.dataset.organizationalUnitShortCode = data[0].oe_kurzbz;
button.dataset.studyPlanId = data[0].studienplan_id;
container.append(button);
return container;
},
],
};
return options;
},
tabulatorEvents() {
const events = [
{
event: "tableBuilt",
event: "renderComplete",
handler: async () => {
const setHeader = (field, text) => {
const col =
this.$refs.classTimeSlotValidityPeriodsTable.tabulator.getColumn(
field,
);
if (!col) return;
document
.querySelectorAll(".fhc-btn-for-org-unit-grouping")
.forEach((button) => {
button.addEventListener("click", (e) => {
let organizationalUnitShortCode =
button.dataset.organizationalUnitShortCode;
const el = col.getElement();
if (!el || !el.querySelector) return;
this.$router.push({
name: "validityPeriodsOverviewByOrganizationUnit",
params: {
organizationalUnitShortCode,
},
});
});
});
const titleEl = el.querySelector(".tabulator-col-title");
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader("nummer", this.$p.t("wawi", "nummer"));
setHeader("anmerkung", this.$p.t("global", "anmerkung"));
setHeader("retouram", this.$p.t("wawi", "retourdatum"));
setHeader("beschreibung", this.$p.t("global", "beschreibung"));
setHeader("kaution", this.$p.t("infocenter", "kaution"));
setHeader("ausgegebenam", this.$p.t("wawi", "ausgabedatum"));
setHeader("person_id", this.$p.t("person", "person_id"));
setHeader("uid", this.$p.t("person", "uid"));
document
.querySelectorAll(".fhc-btn-for-org-unit-and-study-plan-grouping")
.forEach((button) => {
button.addEventListener("click", (e) => {
let organizationalUnitShortCode =
button.dataset.organizationalUnitShortCode;
let studyPlanId = button.dataset.studyPlanId;
this.$router.push({
name: "validityPeriodsOverviewByOrganizationUnitAndStudyPlan",
params: {
organizationalUnitShortCode,
studyPlanId,
},
});
});
});
},
},
];
@@ -174,6 +252,41 @@ export default {
},
},
methods: {
test() {
alert("test");
},
async getParsedClassTimeSlotValidityPeriodData() {
let getAllClassTimeValidityPeriodsResponse = await this.$api.call(
ApiClassSchedule.getAllClassTimeValidityPeriods(),
);
if (getAllClassTimeValidityPeriodsResponse.meta.status === "success") {
let generalWord = this.$p.t("ui", "general");
return getAllClassTimeValidityPeriodsResponse.data.map(
function (period) {
period.organisationseinheit_bezeichnung_extended =
period.organisationseinheit_bezeichnung +
" - " +
period.organisationseinheit_organisationseinheittyp_kurzbz;
if (!period.studienplan_bezeichnung) {
period.studienplan_bezeichnung = generalWord;
}
period.unterrichtszeiten_typ_bezeichnung_mehrsprachig =
period.unterrichtszeiten_typ_bezeichnung_mehrsprachig[0]?.split(
":",
)[1];
return {
...period,
};
},
);
} else {
console.error(
"Error fetching class time slot validity periods:",
getAllClassTimeValidityPeriodsResponse.meta.message,
);
}
},
showClassTimeSlotValidityPeriodModal() {
this.isClassTimeSlotValidityPeriodModalVisible = true;
},
@@ -227,12 +340,12 @@ export default {
},
mounted() {
this.$p
.loadCategory(["global", "lehre", "ui", "gruppenmanagement"])
.loadCategory(["global", "lehre", "ui", "gruppenmanagement", "core"])
.then(() => {
this.phrasesLoaded = true;
});
},
template: /* html */`
template: /* html */ `
<div class="container mt-4">
<h1 class='mb-5'>{{ $p.t("ui", "classScheduleOverviewHeading") }}</h1>
<div class="row mb-3">
@@ -265,7 +378,7 @@ export default {
table-only
:side-menu="false"
:tabulator-options="tabulatorOptions"
:tabulator-events="[{ event: 'cellEdited'}]"
:tabulator-events="tabulatorEvents"
:new-btn-label="$p.t('ui', 'addClassTimeSlotValidityPeriodButton')"
new-btn-show
reload
@@ -21,6 +21,7 @@ export default {
required: true,
},
},
emits: ["hideBsModal", "classTimeSlotTypeCreated", "classTimeSlotTypeUpdated"],
watch: {
isVisible(newValue) {
if (newValue) {
@@ -1,6 +1,8 @@
import ApiClassSchedule from "../../../js/api/factory/classSchedule.js";
import ApiStudienPlan from "../../../js/api/factory/studienplan.js";
import ApiStudienSemester from "../../../js/api/factory/studiensemester.js";
import ApiOrganizationalUnit from "../../../js/api/factory/organizationalUnit.js";
import { formatDate } from "../../helpers/DateHelpers.js";
import BsModal from "../Bootstrap/Modal.js";
import CoreForm from "../Form/Form.js";
@@ -66,6 +68,35 @@ export default {
this.$fhcAlert.handleSystemError(error);
});
},
async "classTimeSlotValidityPeriodFormData.organizationalUnitShortCode"(
newValue,
) {
if (!newValue) {
this.classTimeSlotValidityPeriodFormData.studyPlanId = null;
return;
}
this.refetchFilterableOptions();
},
async "classTimeSlotValidityPeriodFormData.validityPeriodFrom"(newValue) {
if (!newValue) {
this.classTimeSlotValidityPeriodFormData.studyPlanId = null;
return;
}
this.refetchFilterableOptions();
},
async "classTimeSlotValidityPeriodFormData.validityPeriodTo"(newValue) {
if (!newValue) {
this.classTimeSlotValidityPeriodFormData.studyPlanId = null;
return;
}
this.refetchFilterableOptions();
},
async "classTimeSlotValidityPeriodFormData.studyPlanId"(newValue) {
this.refetchFilterableOptions();
},
},
data: () => {
return {
@@ -73,6 +104,8 @@ export default {
isEditInProgress: false,
organizationalUnits: [],
studyPlans: [],
studySemesters: [],
studySemestersByNumber: [],
classTimeSlotTypes: [],
classTimeSlotValidityPeriodFormData: {
id: null,
@@ -86,7 +119,157 @@ export default {
},
};
},
computed: {
isStudyPlanSelectDisabled() {
return (
!this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode ||
!this.classTimeSlotValidityPeriodFormData.validityPeriodFrom ||
!this.classTimeSlotValidityPeriodFormData.validityPeriodTo
);
},
formattedValidityPeriodFrom() {
if (!this.classTimeSlotValidityPeriodFormData.validityPeriodFrom)
return null;
return formatDate(
this.classTimeSlotValidityPeriodFormData.validityPeriodFrom,
);
},
formattedValidityPeriodTo() {
if (!this.classTimeSlotValidityPeriodFormData.validityPeriodTo)
return null;
return formatDate(
this.classTimeSlotValidityPeriodFormData.validityPeriodTo,
);
},
},
methods: {
async refetchFilterableOptions() {
this.studyPlans = await this.getTargetedStudyPlans(
this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode,
this.formattedValidityPeriodFrom,
this.formattedValidityPeriodTo,
);
let studySemestersByDates =
await this.getStudySemestersByOrganizationalUnitAndDates(
this.classTimeSlotValidityPeriodFormData.organizationalUnitShortCode,
this.formattedValidityPeriodFrom,
this.formattedValidityPeriodTo,
);
let studySemesters = new Array(
...new Set(
studySemestersByDates
.map((s) => s.semester_numbers)
.flat()
.sort((a, b) => a - b),
),
);
this.studySemestersByNumber = studySemesters;
if (this.classTimeSlotValidityPeriodFormData.studyPlanId) {
let studySemestersByStudyProgramId =
await this.getStudySemestersByStudyPlanAndDates(
this.classTimeSlotValidityPeriodFormData.studyPlanId,
this.formattedValidityPeriodFrom,
this.formattedValidityPeriodTo,
);
studySemestersByStudyProgramId = new Array(
...new Set(
studySemestersByStudyProgramId
.map((s) => s.semester_numbers)
.flat()
.sort((a, b) => a - b),
),
);
studySemesters = studySemestersByStudyProgramId;
}
this.studySemestersByNumber = studySemesters;
},
async getTargetedStudyPlans(
organizationalUnitShortCode,
validityPeriodFrom,
validityPeriodTo,
) {
if (
!organizationalUnitShortCode ||
!validityPeriodFrom ||
!validityPeriodTo
) {
return [];
}
let getAllStudyPlansResponse = await this.$api.call(
ApiStudienPlan.getStudyPlansByOrganizationalUnitAndSemesterDates(
organizationalUnitShortCode,
validityPeriodFrom,
validityPeriodTo,
),
);
if (getAllStudyPlansResponse.meta.status !== "success") {
console.error(
"Error fetching study plans:",
getAllStudyPlansResponse.meta.message,
);
}
return getAllStudyPlansResponse.data?.length
? getAllStudyPlansResponse.data
: [];
},
async getStudySemestersByOrganizationalUnitAndDates(
organizationalUnitShortCode,
validityPeriodFrom,
validityPeriodTo,
) {
if (
!organizationalUnitShortCode ||
!validityPeriodFrom ||
!validityPeriodTo
) {
return [];
}
let getStudySemestersResponse = await this.$api.call(
ApiStudienSemester.getStudySemestersByOrganizationalUnitAndDates(
organizationalUnitShortCode,
validityPeriodFrom,
validityPeriodTo,
),
);
if (getStudySemestersResponse.meta.status !== "success") {
console.error(
"Error fetching study semesters by organizational unit and dates:",
getStudySemestersResponse.meta.message,
);
}
return getStudySemestersResponse.data?.length
? getStudySemestersResponse.data
: [];
},
async getStudySemestersByStudyPlanAndDates(studyProgramId, validityPeriodFrom, validityPeriodTo) {
if (!studyProgramId || !validityPeriodFrom || !validityPeriodTo) {
return [];
}
let getStudySemestersResponse = await this.$api.call(
ApiStudienSemester.getStudySemestersByStudyPlanAndDates(studyProgramId, validityPeriodFrom, validityPeriodTo),
);
if (getStudySemestersResponse.meta.status !== "success") {
console.error(
"Error fetching study semesters:",
getStudySemestersResponse.meta.message,
);
}
return getStudySemestersResponse.data?.length
? getStudySemestersResponse.data
: [];
},
createClassTimeSlotValidityPeriod() {
return this.$refs.classTimeSlotValidityPeriodData
.call(
@@ -153,7 +336,9 @@ export default {
ApiOrganizationalUnit.getAllOrganizationalUnits(),
);
if (getAllOrganizationalUnitsResponse.meta.status === "success") {
this.organizationalUnits = getAllOrganizationalUnitsResponse.data;
this.organizationalUnits = getAllOrganizationalUnitsResponse.data.sort(
(a, b) => a.bezeichnung.localeCompare(b.bezeichnung),
);
} else {
console.error(
"Error fetching organizational units:",
@@ -197,7 +382,7 @@ export default {
);
}
},
template: `
template: /*html*/ `
<bs-modal ref="classTimeSlotValidityPeriodModal" @hideBsModal="() => { $emit('hideBsModal'); resetClassTimeSlotValidityPeriodModal(); }" size="md">
<template #title>
<p v-if="!classTimeSlotValidityPeriodFormData.id" class="fw-bold mt-3">{{$p.t('ui', 'addClassTimeSlotValidityPeriodModalTitle')}}</p>
@@ -207,33 +392,82 @@ export default {
<form-validation />
<div class="row mb-3">
<form-input
v-model="classTimeSlotValidityPeriodFormData.organizationalUnitShortCode"
:label="$p.t('lehre/organisationseinheit') + ' *'"
type="select"
name="organizationalUnitShortCode"
:label="$p.t('lehre/organisationseinheit') + ' *'"
v-model="classTimeSlotValidityPeriodFormData.organizationalUnitShortCode"
>
<option
v-for="organizationalUnit in organizationalUnits"
:key="organizationalUnit.oe_kurzbz"
:value="organizationalUnit.oe_kurzbz"
>
{{organizationalUnit.bezeichnung}}
{{organizationalUnit.bezeichnung}} - {{ organizationalUnit.organisationseinheittyp_kurzbz }}
</option>
</form-input>
</div>
<div class="row mb-3">
<div class="col-12 mb-3">
<label>{{$p.t('ui', 'validityPeriod')}}</label>
</div>
<div class="col">
<form-input
v-model="classTimeSlotValidityPeriodFormData.validityPeriodFrom"
:label="$p.t('ui/von') + ' *'"
:teleport="true"
:enable-time-picker="false"
type="datePicker"
name="validityPeriodFrom"
format="dd.MM.yyyy"
auto-apply
/>
</div>
<div class="col">
<form-input
v-model="classTimeSlotValidityPeriodFormData.validityPeriodTo"
:label="$p.t('global/bis') + ' *'"
:teleport="true"
:enable-time-picker="false"
type="datePicker"
name="validityPeriodTo"
format="dd.MM.yyyy"
auto-apply
/>
</div>
</div>
<div class="row mb-3">
<form-input
v-model="classTimeSlotValidityPeriodFormData.studyPlanId"
:label="$p.t('lehre/studienplan')"
:disabled="isStudyPlanSelectDisabled"
type="select"
name="studyPlan"
>
<option :value="null"> - </option>
<option
v-for="studyPlan in studyPlans"
:key="studyPlan.studienplan_id"
:value="studyPlan.studienplan_id"
>
{{studyPlan.bezeichnung}} - {{studyPlan.studienplan_id}}
</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
type="select"
name="studyPlan"
:label="$p.t('lehre/studienplan')"
v-model="classTimeSlotValidityPeriodFormData.studyPlanId"
id="ausbildungssemester"
name="semester"
:disabled="isStudyPlanSelectDisabled"
:label="$p.t('lehre', 'ausbildungssemester')+ '*'"
v-model="classTimeSlotValidityPeriodFormData.semester"
>
<option
v-for="studyPlan in studyPlans"
:key="studyPlan.studienplan_id"
:value="studyPlan.studienplan_id"
v-for="studySemester in studySemestersByNumber"
:key="studySemester"
:value="studySemester"
>
{{studyPlan.orgform_kurzbz}} - {{studyPlan.sprache}} - {{studyPlan.semesterwochen}}
{{studySemester}}
</option>
</form-input>
</div>
@@ -249,42 +483,10 @@ export default {
:key="classTimeSlotType.unterrichtszeitentyp_kurzbz"
:value="classTimeSlotType.unterrichtszeitentyp_kurzbz"
>
{{classTimeSlotType.unterrichtszeitentyp_kurzbz}} - {{classTimeSlotType.bezeichnung_mehrsprachig[0].value}} / ({{classTimeSlotType.bezeichnung_mehrsprachig[1].value}})
{{classTimeSlotType.bezeichnung_mehrsprachig[0].value}} / ({{classTimeSlotType.bezeichnung_mehrsprachig[1].value}})
</option>
</form-input>
</div>
<div class="row mb-3">
<div class="col-12 mb-3">
<label>{{$p.t('ui', 'validityPeriod')}}</label>
</div>
<div class="col">
<form-input
type="date"
name="validityPeriodFrom"
:label="$p.t('ui/von') + ' *'"
v-model="classTimeSlotValidityPeriodFormData.validityPeriodFrom"
/>
</div>
<div class="col">
<form-input
type="date"
name="validityPeriodTo"
:label="$p.t('global/bis') + ' *'"
v-model="classTimeSlotValidityPeriodFormData.validityPeriodTo"
/>
</div>
</div>
<div class="row mb-3">
<form-input
type="select"
id="ausbildungssemester"
name="semester"
:label="$p.t('lehre', 'ausbildungssemester')+ '*'"
v-model="classTimeSlotValidityPeriodFormData.semester"
>
<option v-for="sem in Array.from({length:8}).map((u,i) => i+1)" :key="sem" :value="sem">{{sem}}. Semester</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
type="textarea"
@@ -248,6 +248,7 @@ export default {
</div>
</div>
<h2>{{ classScheduleValidityPeriodStartDate }} - {{ classScheduleValidityPeriodEndDate }}</h2>
<p>{{ $p.t("lehre", "ausbildungssemester") }}: {{ classTimeSlotValidityPeriod?.ausbildungssemester }}</p>
<p v-html="classTimeSlotValidityPeriod?.anmerkung"></p>
</div>
<div>
@@ -0,0 +1,114 @@
import ClassScheduleCalendarSelector from "./ClassScheduleCalendarSelector.js";
export default {
name: "ClassScheduleValidityPeriodPreview",
components: {
ClassScheduleCalendarSelector,
},
props: {
classScheduleValidityPeriod: {
type: Object,
required: true,
},
classTimeSlots: {
type: Array,
required: true,
},
classTimeSlotTypes: {
type: Array,
required: true,
},
},
data: () => {
return {};
},
computed: {
classScheduleValidityPeriodStartDate() {
if (!this.$props.classScheduleValidityPeriod) return null;
let dateParts = this.$props.classScheduleValidityPeriod.gueltig_von
.split("-")
.reverse();
return dateParts.join("/");
},
classScheduleValidityPeriodEndDate() {
if (!this.$props.classScheduleValidityPeriod) return null;
let dateParts = this.$props.classScheduleValidityPeriod.gueltig_bis
.split("-")
.reverse();
return dateParts.join("/");
},
classScheduleValidityPeriodStudyPlan() {
return this.$props.classScheduleValidityPeriod.studienplan_bezeichnung;
},
parsedClassTimeSlots() {
let classTimeSlotsGroupedByWeek = [];
this.$props.classTimeSlots.forEach((slot) => {
let groupIdentifikator = slot["unterrichtszeit_gruppe_identifikator"];
let existingGroup = classTimeSlotsGroupedByWeek.find(
(group) => group.groupIdentifikator === groupIdentifikator,
);
if (existingGroup) {
existingGroup.slots.push(slot);
} else {
classTimeSlotsGroupedByWeek.push({
groupIdentifikator,
slots: [slot],
});
}
});
return classTimeSlotsGroupedByWeek;
},
},
methods: {
showClassScheduleValidityPeriod(classScheduleValidityPeriodId) {
this.$router.push({
name: "validityPeriodOverview",
params: {
classTimeSlotValidityPeriodId: classScheduleValidityPeriodId,
},
});
},
},
async created() {},
template: /* html */ `
<div class="container mt-4">
<div class='py-3 d-flex align-items-center justify-content-between'>
<div>
<h2>{{ classScheduleValidityPeriodStartDate }} - {{ classScheduleValidityPeriodEndDate }}</h2>
<h4>{{ classScheduleValidityPeriodStudyPlan }}</h4>
</div>
<a
@click="showClassScheduleValidityPeriod($props.classScheduleValidityPeriod.unterrichtszeitengueltigkeit_id)"
class="ml-auto btn btn-link"
><i class="fa fa-eye fs-5"></i></a>
</div>
<div class='py-3 mb-4'>
<div v-if="parsedClassTimeSlots && parsedClassTimeSlots.length > 0">
<div v-for="(classTimeSlotsPerWeek, index) in parsedClassTimeSlots" :key="index" class="row border-top rounded pt-1 pb-5">
<class-schedule-calendar-selector
:class-time-slot-types="this.classTimeSlotTypes"
:edited-overlays="classTimeSlotsPerWeek.slots.map((slot) => {
return {
databaseId: slot.id,
id: slot.identifier,
weekday: slot.wochentag,
type: slot.unterrichtszeitentyp_kurzbz,
startTime: slot.uhrzeit_von,
endTime: slot.uhrzeit_bis,
};
})"
:isPreviewMode="true"
/>
</div>
</div>
<div v-else class="d-flex align-items-center justify-content-center border rounded p-2">
<p>{{ $p.t("ui", "noClassScheduleValidityPeriodTimeSlotsFound") }}</p>
</div>
</div>
</div>
`,
};
+1 -1
View File
@@ -37,7 +37,7 @@ export function formatDate(d) {
// parameter is of type Date
if(d instanceof Date)
{
if (isNaN(date.valueOf())) {
if (isNaN(d.valueOf())) {
return 'N/A';
}
// if the date is an invalid string then creating a date from the string will fail and N/A is returned
+87 -8
View File
@@ -57279,7 +57279,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'montag',
'phrase' => 'monday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -57299,7 +57299,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'dienstag',
'phrase' => 'tuesday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -57319,7 +57319,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'mittwoch',
'phrase' => 'wednesday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -57339,7 +57339,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'donnerstag',
'phrase' => 'thursday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -57359,7 +57359,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'freitag',
'phrase' => 'friday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -57379,7 +57379,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'samstag',
'phrase' => 'saturday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -57399,7 +57399,7 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'sonntag',
'phrase' => 'sunday',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -58336,7 +58336,86 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
// ### Phrases Dashboard Admin END
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'general',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'allgemein',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'general',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'classScheduleOrgUnitGroupedValidityPeriodsOverviewTitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Übersicht Unterrichtszeit Gültigkeitszeiträume nach Organisationseinheit gruppiert',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Overview of class schedule validity periods grouped by organizational unit',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'classScheduleStudyPlanGroupedValidityPeriodsOverviewTitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Übersicht Unterrichtszeit Gültigkeitszeiträume nach Studienplan gruppiert',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Overview of class schedule validity periods grouped by study plan',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'selectClassTimeSlotTypeTitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Übersicht Unterrichtszeit Gültigkeitszeiträume nach Studienplan gruppiert',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Select class time slot type',
'description' => '',
'insertvon' => 'system'
)
)
),
);