From 8ca50adb6694780d8a638f335813faf2b09dbf5b Mon Sep 17 00:00:00 2001 From: tamaro Date: Tue, 24 Mar 2026 18:57:08 +0100 Subject: [PATCH 01/12] WIP - redesign request.php --- lang/de/local_lsf_unification.php | 2 +- lang/en/local_lsf_unification.php | 2 +- request.php | 127 ++++++------------ templates/courserequest/coursetable.mustache | 66 +++++++++ templates/courserequest/final.mustache | 32 +++++ templates/courserequest/helptext.mustache | 29 ++++ .../courserequest/request_handler.mustache | 44 ++++++ templates/first_overview.mustache | 78 ++++++----- 8 files changed, 259 insertions(+), 121 deletions(-) create mode 100644 templates/courserequest/coursetable.mustache create mode 100644 templates/courserequest/final.mustache create mode 100644 templates/courserequest/helptext.mustache create mode 100644 templates/courserequest/request_handler.mustache diff --git a/lang/de/local_lsf_unification.php b/lang/de/local_lsf_unification.php index fa85cf7..11d361d 100644 --- a/lang/de/local_lsf_unification.php +++ b/lang/de/local_lsf_unification.php @@ -27,7 +27,7 @@ $string['already_requested'] = 'Diesr Kurs wurde bereits angefragt. Der/die Lehrende muss erst die bestehende Anfrage beantworten, bevor neue Anfragen getätigt werden können.'; $string['answer_course_found'] = "Der zu beantragende Kurs befindet sich in der nachfolgenden Liste:"; $string['answer_course_in_lsf_and_visible'] = "Der zu beantragende Kurs befindet sich im HIS-LSF und Sie sind dort als Lehrende(r) eingetragen."; -$string['answer_course_in_lsf_but_invisible'] = 'Der zu beantragende Kurs befindet sich im HIS-LSF und der Kurs wird oben nicht angezeigt, obwohl der Nutzer mit der Kennung {$a} als Lehrende(r) für den Kurs eingetragen ist.'; +$string['answer_course_in_lsf_but_invisible'] = 'Der zu beantragende Kurs befindet sich im HIS-LSF und der Kurs wird oben nicht angezeigt, obwohl der Lehrende für den Kurs eingetragen ist.'; $string['answer_goto_old_requestform'] = "Sie möchten eine Kursbeantragung ohne Übernahme der Daten aus dem HIS-LSF vornehmen."; $string['answer_proxy_creation'] = "Der Kurs existiert im HIS-LSF und Sie möchten im Auftrag einer/s dort eingetragenen Lehrenden diesen Kurs erstellen."; $string['answer_sent'] = 'Vielen Dank für das Verarbeiten dieser Anfrage. Ihre Entscheidung wird dem Anfragenden automatisch per Email mitgeteilt.'; diff --git a/lang/en/local_lsf_unification.php b/lang/en/local_lsf_unification.php index daf6c3a..ebc807e 100644 --- a/lang/en/local_lsf_unification.php +++ b/lang/en/local_lsf_unification.php @@ -29,7 +29,7 @@ $string['already_requested'] = 'Sorry, this course was already requested and the teacher has to reply to this request before a new request can be issued'; $string['answer_course_found'] = "The following list contains the course I'm looking for:"; $string['answer_course_in_lsf_and_visible'] = "The course exists on the HIS-LSF platform and you are registered as a teacher for that course."; -$string['answer_course_in_lsf_but_invisible'] = 'The course is not listed above, but exists on the HIS-LSF platform and {$a} is registered as a teacher for that course.'; +$string['answer_course_in_lsf_but_invisible'] = 'The course is not listed above, but exists on the HIS-LSF platform and the teacher is registered as a teacher for that course.'; $string['answer_goto_old_requestform'] = "None of the above apply and you are authorized to create this course in the Learnweb."; $string['answer_proxy_creation'] = "The course exists on the HIS-LSF platform and you are authorized to create this course on behalf of a registered teacher."; $string['answer_sent'] = 'Thank you for processing this request, your decision will be sent to the requester.'; diff --git a/request.php b/request.php index 23f7fe0..3347311 100644 --- a/request.php +++ b/request.php @@ -102,48 +102,28 @@ function ($course) { * @throws coding_exception */ function print_helptext(string $t, string|null $s = null): void { - $answerstr = get_string('answer_' . $t, 'local_lsf_unification'); - $infostr = get_string('info_' . $t, 'local_lsf_unification', $s, true); - echo "" . $answerstr . "
" . $infostr; - echo "
" . get_string('back', 'local_lsf_unification') . ""; -} - -/** - * Print the courses the user can see. - * @return void - * @throws coding_exception - */ -function print_courseselection(): void { - global $USER, $answer; - echo "
"; - print_coursetable($USER->username); - echo "
"; - echo "
" . get_string('back', 'local_lsf_unification') . ""; + global $OUTPUT; + $mustachedata = [ + 'answerstr' => get_string('answer_' . $t, 'local_lsf_unification'), + 'infostr' => get_string('info_' . $t, 'local_lsf_unification', $s), + ]; + echo $OUTPUT->render_from_template('local_lsf_unification/courserequest/helptext', $mustachedata); } /** * Print the course table for a teacher. * @param string $teacher - * @param string $appendix * @return void * @throws coding_exception */ -function print_coursetable(string $teacher, string $appendix = ""): void { - echo ""; - foreach (get_teachers_course_list($teacher, true) as $course) { - if (!course_exists($course->veranstid)) { - $veranstid = $course->veranstid; - echo " - - - "; - } - } - echo $appendix . "
" . get_string('choose_course', 'local_lsf_unification') . "
- - - -
"; +function print_coursetable(string $teacher, array $options = []): void { + global $OUTPUT, $answer; + $mustachedata = [ + 'courses' => array_values(get_teachers_course_list($teacher, true)), + 'answer' => $answer, + ]; + $mustachedata = array_merge($mustachedata, $options); + echo $OUTPUT->render_from_template('local_lsf_unification/courserequest/coursetable', $mustachedata); } /** @@ -153,20 +133,12 @@ function print_coursetable(string $teacher, string $appendix = ""): void { */ function print_final() { global $OUTPUT, $CFG, $courseid; - $nextsteps = get_string('next_steps', 'local_lsf_unification'); - $linktext = [ - 'users' => get_string('linktext_users', 'local_lsf_unification'), - 'content' => get_string('linktext_content', 'local_lsf_unification'), - 'course' => get_string('linktext_course', 'local_lsf_unification'), - ]; - $href = [ - 0 => $CFG->wwwroot . "/user/index.php?id=" . ($courseid), - 1 => $CFG->wwwroot . "/backup/import.php?id=" . ($courseid), - 2 => $CFG->wwwroot . "/course/view.php?id=" . ($courseid), + $mustachedata = [ + 'userlink' => new moodle_url("/user/index.php", ['id' => $courseid]), + 'backuplink' => new moodle_url("/backup/import.php", ['id' => $courseid]), + 'courselink' => new moodle_url("/course/view.php", ['id' => $courseid]), ]; - echo $OUTPUT->box("" . $nextsteps . ":
" . $linktext['users'] . "
- " . $linktext['content'] . "
- " . $linktext['course'] . "
 "); + echo $OUTPUT->render_from_template('local_lsf_unification/courserequest/final', $mustachedata); } @@ -177,32 +149,18 @@ function print_final() { * @throws dml_exception */ function print_remote_creation() { - global $USER, $answer, $teachername, $veranstid; + global $teachername, $veranstid; if (!get_config('local_lsf_unification', 'remote_creation')) { return; } if (empty($veranstid)) { - echo "
"; - if (empty($teachername)) { - $chooseteacher = get_string('choose_teacher', 'local_lsf_unification'); - echo "" . $chooseteacher . ""; - } else { - echo ""; - $answercourselsf = get_string('answer_course_in_lsf_but_invisible', 'local_lsf_unification', $teachername); - $appendix = " - - - - - - - - "; - print_coursetable($teachername, $appendix); - } - $select = get_string('select', 'local_lsf_unification'); - $back = get_string('back', 'local_lsf_unification'); - echo "

" . $back . ""; + $mustachedata = [ + 'emptyteacher' => empty($teachername), + 'remote' => true, + 'teachername' => $teachername, + 'appendix' => true, + ]; + print_coursetable($teachername, $mustachedata); } else { if ($veranstid < 0) { echo get_string('his_info', 'local_lsf_unification'); @@ -275,7 +233,7 @@ function print_coursecreation() { $warnings = get_string('warnings', 'local_lsf_unification'); $out = "

" . $OUTPUT->box("" . $warnings . "
" . "

" . $result["warnings"] . "
") . "

"; echo (!empty($result["warnings"])) ? $out : ""; - print_final($result["course"]->id); + print_final(); } else { $editform->display(); } @@ -288,21 +246,18 @@ function print_coursecreation() { * @throws dml_exception */ function print_request_handler() { - global $CFG, $DB, $answer, $request, $veranstid, $accept; + global $DB, $answer, $request, $veranstid, $accept, $OUTPUT; $course = get_course_by_veranstid($veranstid); $requester = $DB->get_record("user", ["id" => $request->requesterid]); - if (empty($accept)) { - echo get_string('remote_request_select_alternative', 'local_lsf_unification'); - $params = new stdClass(); - $params->a = $requester->firstname . " " . $requester->lastname; - $params->b = mb_convert_encoding($course->titel, 'UTF-8', 'ISO-8859-1'); - $href = $CFG->wwwroot . "/local/lsf_unification/request.php?answer=" . $answer . "&requestid=" . $request->id; - $remoteaccept = get_string('remote_request_accept', 'local_lsf_unification', $params); - $remotedeclined = get_string('remote_request_decline', 'local_lsf_unification', $params); - echo '

' . - "" . $remoteaccept . "" . "
"; - echo "" . $remotedeclined . "" . "
"; - } else { + $basepath = '/local/lsf_unification/request.php'; + $mustachedata = [ + 'emptyaccept' => empty($accept), + 'link1' => new moodle_url($basepath, ['answer' => $answer, 'requestid' => $request->id, 'accept' => 1]), + 'link2' => new moodle_url($basepath, ['answer' => $answer, 'requestid' => $request->id, 'accept' => 2]), + 'param_a' => $requester->firstname . " " . $requester->lastname, + 'param_b' => mb_convert_encoding($course->titel, 'UTF-8', 'ISO-8859-1'), + ]; + if (!empty($accept)) { if ($accept == 1) { set_course_accepted($veranstid); send_course_creation_mail($requester, $course); @@ -310,19 +265,17 @@ function print_request_handler() { set_course_declined($veranstid); send_sorry_mail($requester, $course); } - echo get_string('answer_sent', 'local_lsf_unification'); } + echo $OUTPUT->render_from_template('local_lsf_unification/courserequest/request_handler', $mustachedata); } - // Handle Course-Request. - if (establish_secondary_DB_connection() === true) { if (empty($answer)) { print_first_overview(); // Task Selection. } else if ($answer == 1) { if (empty($veranstid)) { - print_courseselection(); // Extern Course Selection. + print_coursetable($USER->username); } else { if (has_course_import_rights($veranstid, $USER)) { // Validate veranstid, user. print_coursecreation(); // Request neccessary details and create course. diff --git a/templates/courserequest/coursetable.mustache b/templates/courserequest/coursetable.mustache new file mode 100644 index 0000000..0400c51 --- /dev/null +++ b/templates/courserequest/coursetable.mustache @@ -0,0 +1,66 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_lsf_unification/courserequest/coursetable + + This template is part of the page to request a new course. + + Example (json): + { + + } +}} +

+ + + {{#emptyteacher}} + {{#str}} choose_teacher, local_lsf_unification {{/str}} + + {{/emptyteacher}} + {{^emptyteacher}} + {{#remote}} + + {{/remote}} + {{! The courselist}} +
+
+
{{#str}} choose_course, local_lsf_unification {{/str}}
+
+
+
+ {{#courses}} + + {{/courses}} + {{#appendix}} + + {{/appendix}} +
+
+
+ {{/emptyteacher}} + + {{! Submit }} + +
+
+{{#str}} back, local_lsf_unification {{/str}} diff --git a/templates/courserequest/final.mustache b/templates/courserequest/final.mustache new file mode 100644 index 0000000..070de79 --- /dev/null +++ b/templates/courserequest/final.mustache @@ -0,0 +1,32 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_lsf_unification/courserequest/final + + This template is part of the page to request a new course. + + Example (json): + { + + } +}} +
+ {{#str}} next_steps, local_lsf_unification {{/str}}
+ {{#str}} linktext_users, local_lsf_unification {{/str}}
+ {{#str}} linktext_content, local_lsf_unification {{/str}}
+ {{#str}} linktext_course, local_lsf_unification {{/str}}
+
diff --git a/templates/courserequest/helptext.mustache b/templates/courserequest/helptext.mustache new file mode 100644 index 0000000..ad07578 --- /dev/null +++ b/templates/courserequest/helptext.mustache @@ -0,0 +1,29 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_lsf_unification/courserequest/helptext + + This template is part of the page to request a new course. + + Example (json): + { + + } +}} +{{answerstr}}
+{{{infostr}}}
+{{#str}} back, local_lsf_unification {{/str}} \ No newline at end of file diff --git a/templates/courserequest/request_handler.mustache b/templates/courserequest/request_handler.mustache new file mode 100644 index 0000000..0d40d81 --- /dev/null +++ b/templates/courserequest/request_handler.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template local_lsf_unification/courserequest/request_handler + + This template is part of the page to request a new course. + + Example (json): + { + + } +}} +{{#empty_accept}} + {{#str}} remote_request_select_alternative, local_lsf_unification {{/str}} + + {{#str}} remote_request_accept, local_lsf_unification, + {"a": {{#quote}}{{param_a}}{{/quote}}, "b": {{#quote}}{{param_b}}{{/quote}} } + {{/str}} + +
+ + {{#str}} remote_request_decline, local_lsf_unification, + {"a": {{#quote}}{{param_a}}{{/quote}}, "b": {{#quote}}{{param_b}}{{/quote}} } + {{/str}} + +
+{{/empty_accept}} +{{^empty_accept}} + {{#str}} answer_sent, local_lsf_unification {{/str}} +{{/empty_accept}} \ No newline at end of file diff --git a/templates/first_overview.mustache b/templates/first_overview.mustache index dee4323..399780a 100644 --- a/templates/first_overview.mustache +++ b/templates/first_overview.mustache @@ -36,39 +36,53 @@ }}

{{#str}} notice, local_lsf_unification {{/str}}

+
-

{{#str}} question, local_lsf_unification {{/str}}

-
- - -
    - {{#courselist}} -
  • {{{title}}}
  • - {{/courselist}} -
-
-
- - -
- {{#showremotecreation}} -
- - +
+
+
{{#str}} question, local_lsf_unification {{/str}}
- {{/showremotecreation}} -
- - +
+
+ {{! Option 1: Course found }} + + + {{! Option 2: Course in LSF but not found here }} + + + {{! Option 3: Remote request }} + {{#showremotecreation}} + + {{/showremotecreation}} + + {{! Option 4: Go to old request form }} + +
+
+
+ + {{! Submit button }} +
+
- -
+
\ No newline at end of file From 7223ea7276d5f20ac1be368c90393cd8d009fc82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mei=C3=9Fner?= Date: Tue, 2 Jun 2026 15:44:26 +0200 Subject: [PATCH 02/12] Add interface for campus management systems This introduces an interface that is to be implemented by an LSF and SAP-SLcM connection class. Currently only one function from lib_his.php was added to the interface but eventually every necessary function in lib_his.php relevant to both LSF and SAP-SLcM should be abstracted and added to this interface. --- classes/campus_management.php | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 classes/campus_management.php diff --git a/classes/campus_management.php b/classes/campus_management.php new file mode 100644 index 0000000..1336a9b --- /dev/null +++ b/classes/campus_management.php @@ -0,0 +1,36 @@ +. + +namespace local_lsf_unification; + +use stdClass; + +/** + * Interface to campus management systems (CMS). + * + * @package local_lsf_unification + * @copyright 2026 Daniel Meißner + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface campus_management { + /** + * Query the CMS for a list of courses of a given teacher. + * + * @param stdClass $teacher The user record of the teacher + * @return array A list of course objects + */ + public function courses_of_teacher(stdClass $teacher): array; +} From 77b71039a74eeb6e9298f8ad959bfbe32e658e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mei=C3=9Fner?= Date: Tue, 2 Jun 2026 15:46:47 +0200 Subject: [PATCH 03/12] Add an implementation of campus_management for LSF Add a sample implementation of the campus_management interface for HIS-LSF. Note that this implementation of courses_of_teacher() does not respect the max_import_age admin setting. This needs to be fixed. --- classes/his_lsf.php | 92 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 classes/his_lsf.php diff --git a/classes/his_lsf.php b/classes/his_lsf.php new file mode 100644 index 0000000..10fafce --- /dev/null +++ b/classes/his_lsf.php @@ -0,0 +1,92 @@ +. + +namespace local_lsf_unification; + +use PDO; +use stdClass; + +/** + * Connection to the LSF database. + * + * @package local_lsf_unification + * @copyright 2026 Daniel Meißner + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class his_lsf implements campus_management { + /** @var PDO The database connection to LSF */ + private PDO $db; + public function __construct( + /** @var stdClass The plugin configuration */ + private readonly stdClass $config + ) { + $this->db = new PDO($this->connection_string_from($config)); + } + + private function connection_string_from(stdClass $config): string { + $options = [ + 'host' => $config->dbhost, + 'port' => $config->dbport, + 'user' => $config->dbuser, + 'dbname' => $config->dbname, + ]; + + if ($config->dbusessl) { + $options['sslmode'] = 'verify-full'; + $options['sslrootcert'] = $config->dbsslrootcert; + $options['sslcert'] = $config->dbsslcert; + $options['sslkey'] = $config->dbsslkey; + } else { + $options['password'] = $config->dbpass; + } + + $parts = []; + foreach ($options as $key => $val) { + $escaped = addcslashes((string) $val, "\\'"); + $parts[] = "$key='$escaped'"; + } + return implode(' ', $parts); + } + + #[\Override] + public function courses_of_teacher(stdClass $teacher): array { + $courses = $this->db->query(" +SELECT + lv.veranstid, + lv.veranstnr, + lv.titel, + lv.semestertxt, + lv.urlveranst +FROM + learnweb_veranstaltung lv + JOIN learnweb_personal_veranst lpv ON lpv.veranstid = lv.veranstid + JOIN learnweb_personal lp ON lp.pid = lpv.pid +WHERE + lp.login = '{$teacher->username}' +", + PDO::FETCH_OBJ); + + return array_map( + function($course) { + $course->info = $course->titel . "  (" . + $course->semestertxt . + ' KVV-Nr. ' . $course->veranstnr . ')'; + return $course; + }, + $courses->fetchAll() + ); + } +} From 53ef126ed59a973800e236e8bd91299707d6d937 Mon Sep 17 00:00:00 2001 From: tamaro Date: Tue, 28 Jul 2026 22:42:21 +0200 Subject: [PATCH 04/12] Edit old database tables to make room for new datatable structure --- classes/privacy/provider.php | 18 +++++++++--------- db/install.xml | 4 ++-- db/upgrade.php | 10 ++++++++++ lib.php | 6 +++--- lib_his.php | 30 +++++++++++++++--------------- remoterequests.php | 4 ++-- request.php | 5 ++++- 7 files changed, 45 insertions(+), 32 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 4e1caa3..ead41c5 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -50,7 +50,7 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l */ public static function get_metadata(collection $collection): collection { $collection->add_database_table( - 'local_lsf_unification_course', + 'local_lsf_unification_course_matching', [ 'veranstid' => 'privacy:metadata:local_lsf_unification:veranstid', 'mdlid' => 'privacy:metadata:local_lsf_unification:mdlid', @@ -76,7 +76,7 @@ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); $sql = "SELECT * - FROM {local_lsf_unification_course} + FROM {local_lsf_unification_course_matching} WHERE (requesterid = :requesterid OR acceptorid = :acceptorid)"; $contextparams['requesterid'] = $userid; @@ -114,13 +114,13 @@ public static function export_user_data(approved_contextlist $contextlist): void continue; } $sql = "SELECT * - FROM {local_lsf_unification_course} + FROM {local_lsf_unification_course_matching} WHERE (requesterid = :userid)"; $contextparams['userid'] = $user->id; $coursesrequested = $DB->get_recordset_sql($sql, $contextparams); $sql = "SELECT * - FROM {local_lsf_unification_course} + FROM {local_lsf_unification_course_matching} WHERE (acceptorid = :userid)"; $coursesaccepted = $DB->get_recordset_sql($sql, $contextparams); @@ -194,14 +194,14 @@ public static function delete_data_for_all_users_in_context(context $context): v // Sanity check that context is at the System context level. if ($context->contextlevel == CONTEXT_SYSTEM) { - $DB->delete_records("local_lsf_unification_course"); + $DB->delete_records("local_lsf_unification_course_matching"); return; } // Sanity check that context is at the User context level. if ($context->contextlevel == CONTEXT_USER) { $userid = $context->instanceid; - $DB->delete_records("local_lsf_unification_course", ['acceptorid' => $userid]); - $DB->delete_records("local_lsf_unification_course", ['requesterid' => $userid]); + $DB->delete_records("local_lsf_unification_course_matching", ['acceptorid' => $userid]); + $DB->delete_records("local_lsf_unification_course_matching", ['requesterid' => $userid]); } } @@ -225,8 +225,8 @@ public static function delete_data_for_user(approved_contextlist $contextlist): } $userid = $context->instanceid; - $DB->delete_records("local_lsf_unification_course", ['acceptorid' => $userid]); - $DB->delete_records("local_lsf_unification_course", ['requesterid' => $userid]); + $DB->delete_records("local_lsf_unification_course_matching", ['acceptorid' => $userid]); + $DB->delete_records("local_lsf_unification_course_matching", ['requesterid' => $userid]); } } } diff --git a/db/install.xml b/db/install.xml index b0da12b..2ce30f8 100644 --- a/db/install.xml +++ b/db/install.xml @@ -20,7 +20,7 @@ - +
@@ -32,7 +32,7 @@
- +
diff --git a/db/upgrade.php b/db/upgrade.php index 6493dde..b91fe42 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -134,5 +134,15 @@ function xmldb_local_lsf_unification_upgrade(int $oldversion): bool { upgrade_plugin_savepoint(true, 2025123100, 'local', 'lsf_unification'); } + if ($oldversion < 2025123105) { + // Rename course table to make space for new course entities. + $oldtable = new xmldb_table('local_lsf_unification_course'); + if ($dbman->table_exists($oldtable)) { + $dbman->rename_table($oldtable, 'local_lsf_unification_course_matching'); + } + // Savepoint reached. + upgrade_plugin_savepoint(true, 2025123105, 'local', 'lsf_unification'); + } + return true; } diff --git a/lib.php b/lib.php index fe8d36a..8de02a4 100644 --- a/lib.php +++ b/lib.php @@ -91,7 +91,7 @@ function has_course_import_rights(int $veranstid, object $user): bool { if (!is_course_of_teacher($veranstid, $user->username)) { if ( $courseentry = $DB->get_record( - "local_lsf_unification_course", + "local_lsf_unification_course_matching", ["veranstid" => $veranstid, "requesterid" => $user->id, ] ) @@ -125,7 +125,7 @@ function is_course_imported_by(int $mdlid, object $user): bool { global $DB; if ( $courseentry = $DB->get_record( - "local_lsf_unification_course", + "local_lsf_unification_course_matching", ["mdlid" => $mdlid, "requesterid" => $user->id, "requeststate" => 2, ] ) @@ -146,7 +146,7 @@ function get_course_acceptor(int $mdlid): int|null { if ( $courseentry = $DB->get_record( - "local_lsf_unification_course", + "local_lsf_unification_course_matching", ["mdlid" => $mdlid, "requeststate" => 2, ] ) diff --git a/lib_his.php b/lib_his.php index c443635..5608f84 100644 --- a/lib_his.php +++ b/lib_his.php @@ -455,12 +455,12 @@ function get_default_startdate(object $lsfcourse): int { function course_exists(int $veranstid): bool { global $DB; if ( - $DB->record_exists("local_lsf_unification_course", ["veranstid" => ($veranstid)]) && - !($DB->record_exists("local_lsf_unification_course", ["veranstid" => ($veranstid), "mdlid" => 0]) || - $DB->record_exists("local_lsf_unification_course", ["veranstid" => ($veranstid), "mdlid" => 1])) + $DB->record_exists("local_lsf_unification_course_matching", ["veranstid" => ($veranstid)]) && + !($DB->record_exists("local_lsf_unification_course_matching", ["veranstid" => ($veranstid), "mdlid" => 0]) || + $DB->record_exists("local_lsf_unification_course_matching", ["veranstid" => ($veranstid), "mdlid" => 1])) ) { if (!$DB->record_exists("course", ["idnumber" => ($veranstid)])) { - $DB->delete_records("local_lsf_unification_course", ["veranstid" => ($veranstid)]); + $DB->delete_records("local_lsf_unification_course_matching", ["veranstid" => ($veranstid)]); } else { return true; } @@ -551,16 +551,16 @@ function enrole_teachers(int $veranstid, int $courseid): string { */ function set_course_created(int $veranstid, int $courseid): void { global $DB; - if ($courseentry = $DB->get_record("local_lsf_unification_course", ["veranstid" => $veranstid])) { + if ($courseentry = $DB->get_record("local_lsf_unification_course_matching", ["veranstid" => $veranstid])) { $courseentry->mdlid = $courseid; $courseentry->timestamp = time(); - $DB->update_record('local_lsf_unification_course', $courseentry); + $DB->update_record('local_lsf_unification_course_matching', $courseentry); } else { $courseentry = new stdClass(); $courseentry->veranstid = $veranstid; $courseentry->mdlid = $courseid; $courseentry->timestamp = time(); - $DB->insert_record("local_lsf_unification_course", $courseentry); + $DB->insert_record("local_lsf_unification_course_matching", $courseentry); } } @@ -573,7 +573,7 @@ function set_course_created(int $veranstid, int $courseid): void { */ function get_course_request(int $rid): false|stdClass { global $DB; - return $DB->get_record("local_lsf_unification_course", ["id" => $rid, "mdlid" => 0]); + return $DB->get_record("local_lsf_unification_course_matching", ["id" => $rid, "mdlid" => 0]); } /** @@ -584,7 +584,7 @@ function get_course_request(int $rid): false|stdClass { */ function get_course_requests(): array { global $DB; - return $DB->get_records("local_lsf_unification_course", ["mdlid" => 0], "id"); + return $DB->get_records("local_lsf_unification_course_matching", ["mdlid" => 0], "id"); } /** @@ -595,7 +595,7 @@ function get_course_requests(): array { */ function set_course_requested(int $veranstid): bool|int|null { global $DB, $USER; - if ($courseentry = $DB->get_record("local_lsf_unification_course", ["veranstid" => $veranstid])) { + if ($courseentry = $DB->get_record("local_lsf_unification_course_matching", ["veranstid" => $veranstid])) { return null; } else { $courseentry = new stdClass(); @@ -604,7 +604,7 @@ function set_course_requested(int $veranstid): bool|int|null { $courseentry->requeststate = 1; $courseentry->timestamp = time(); $courseentry->requesterid = $USER->id; - return $DB->insert_record("local_lsf_unification_course", $courseentry); + return $DB->insert_record("local_lsf_unification_course_matching", $courseentry); } } @@ -616,11 +616,11 @@ function set_course_requested(int $veranstid): bool|int|null { */ function set_course_accepted(int $veranstid): int|null { global $DB, $USER; - if ($courseentry = $DB->get_record("local_lsf_unification_course", ["veranstid" => $veranstid])) { + if ($courseentry = $DB->get_record("local_lsf_unification_course_matching", ["veranstid" => $veranstid])) { $courseentry->requeststate = 2; $courseentry->timestamp = time(); $courseentry->acceptorid = $USER->id; - $DB->update_record('local_lsf_unification_course', $courseentry); + $DB->update_record('local_lsf_unification_course_matching', $courseentry); return $courseentry->id; } return null; @@ -634,8 +634,8 @@ function set_course_accepted(int $veranstid): int|null { */ function set_course_declined(int $veranstid): void { global $DB, $USER; - if ($courseentry = $DB->get_record("local_lsf_unification_course", ["veranstid" => $veranstid])) { - $DB->delete_records("local_lsf_unification_course", ["veranstid" => ($veranstid)]); + if ($courseentry = $DB->get_record("local_lsf_unification_course_matching", ["veranstid" => $veranstid])) { + $DB->delete_records("local_lsf_unification_course_matching", ["veranstid" => ($veranstid)]); } } diff --git a/remoterequests.php b/remoterequests.php index 1aa30ce..d3d309a 100644 --- a/remoterequests.php +++ b/remoterequests.php @@ -45,7 +45,7 @@ if ($rid != -1 && $action != -1) { if ($action == 1) { - $DB->delete_records("local_lsf_unification_course", ["id" => $rid, "mdlid" => 0]); + $DB->delete_records("local_lsf_unification_course_matching", ["id" => $rid, "mdlid" => 0]); echo $OUTPUT->box("Anfrage (" . $rid . ") geloescht"); } else if ($action == 2 || $action == 3) { $request = get_course_request($rid); @@ -75,7 +75,7 @@ $table->define_baseurl(new url('remoterequests.php')); $table->define_columns(['requestid', 'course', 'requester', 'actions']); $table->define_headers(['Antrag Nr.', 'Veranstaltung', 'Personen', 'Aktionen']); - $requests = $DB->get_records('local_lsf_unification_course', ['mdlid' => 0], 'id'); + $requests = $DB->get_records('local_lsf_unification_course_matching', ['mdlid' => 0], 'id'); $table->setup(); $helpfuntion = function ($arrayel) { diff --git a/request.php b/request.php index 3347311..35fc61a 100644 --- a/request.php +++ b/request.php @@ -71,7 +71,10 @@ echo $OUTPUT->header(); if (!empty($requestid)) { - if (($request = $DB->get_record("local_lsf_unification_course", ["id" => $requestid])) && ($request->requeststate == 1)) { + if ( + ($request = $DB->get_record("local_lsf_unification_course_matching", ["id" => $requestid])) && + ($request->requeststate == 1) + ) { $veranstid = $request->veranstid; } } From 3d65b1b0298c4e97b8972bad7152cb6999a6ce07 Mon Sep 17 00:00:00 2001 From: tamaro Date: Tue, 28 Jul 2026 22:47:55 +0200 Subject: [PATCH 05/12] Add new database tables Add three tables that will be used in the new plugins structure: - courses: stores an abstraction of courses of any cms - course_requests: stores requests for a new course that was done on behalf of a teacher - imported_courses: stores courses that were imported into moodle --- db/install.xml | 45 +++++++++++++++++++++++++++++++++ db/upgrade.php | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ version.php | 2 +- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/db/install.xml b/db/install.xml index 2ce30f8..0837b6f 100644 --- a/db/install.xml +++ b/db/install.xml @@ -47,5 +47,50 @@
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + +
\ No newline at end of file diff --git a/db/upgrade.php b/db/upgrade.php index b91fe42..df38d0b 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -144,5 +144,73 @@ function xmldb_local_lsf_unification_upgrade(int $oldversion): bool { upgrade_plugin_savepoint(true, 2025123105, 'local', 'lsf_unification'); } + if ($oldversion < 2025123106) { + // Define table local_lsf_unification_courses to be created. + $table = new xmldb_table('local_lsf_unification_courses'); + + // Adding fields to table local_lsf_unification_courses. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('cms', XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('instance', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('title', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('shorttitle', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('teacher', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('semester', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('url', XMLDB_TYPE_CHAR, '1333', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table local_lsf_unification_courses. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Adding indexes to table local_lsf_unification_courses. + $table->add_index('cms_instance', XMLDB_INDEX_UNIQUE, ['cms', 'instance']); + + // Conditionally launch create table for local_lsf_unification_courses. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table local_lsf_unification_course_requests to be created. + $table = new xmldb_table('local_lsf_unification_course_requests'); + + // Adding fields to table local_lsf_unification_course_requests. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('requesterid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('state', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table local_lsf_unification_course_requests. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'local_lsf_unification_courses', ['id']); + $table->add_key('requesterid', XMLDB_KEY_FOREIGN, ['requesterid'], 'user', ['id']); + + // Conditionally launch create table for local_lsf_unification_course_requests. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table local_lsf_unification_imported_courses to be created. + $table = new xmldb_table('local_lsf_unification_imported_courses'); + + // Adding fields to table local_lsf_unification_imported_courses. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('moodleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table local_lsf_unification_imported_courses. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('courseid', XMLDB_KEY_FOREIGN_UNIQUE, ['courseid'], 'local_lsf_unification_courses', ['id']); + $table->add_key('moodleid', XMLDB_KEY_FOREIGN, ['moodleid'], 'course', ['id']); + + // Conditionally launch create table for local_lsf_unification_imported_courses. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Savepoint reached. + upgrade_plugin_savepoint(true, 2025123106, 'local', 'lsf_unification'); + } + return true; } diff --git a/version.php b/version.php index ae06e49..3b137ed 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2025123102; +$plugin->version = 2025123106; $plugin->component = 'local_lsf_unification'; $plugin->requires = 2024100700; // Require Moodle 4.5. $plugin->supported = [405, 501]; From 9af095d8cd598ed4fbc5837400c63353c2ba0873 Mon Sep 17 00:00:00 2001 From: Tamaro Walter Date: Sun, 23 Aug 2026 20:23:49 +0200 Subject: [PATCH 06/12] database - request --- db/install.xml | 1 + db/upgrade.php | 1 + 2 files changed, 2 insertions(+) diff --git a/db/install.xml b/db/install.xml index 0837b6f..65da189 100644 --- a/db/install.xml +++ b/db/install.xml @@ -73,6 +73,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index df38d0b..21d9778 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -179,6 +179,7 @@ function xmldb_local_lsf_unification_upgrade(int $oldversion): bool { $table->add_field('requesterid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('state', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('state', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); // Adding keys to table local_lsf_unification_course_requests. $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); From 64b9d79a314ddd2e2919e7cebee1486c481b4ec1 Mon Sep 17 00:00:00 2001 From: tamaro Date: Tue, 28 Jul 2026 23:00:23 +0200 Subject: [PATCH 07/12] Add completely new backend structure The backend has a new classes structure with different substructures: - local/cms: An interface structure with classes that can access a course management systems database and fetch data - local/models: An implementation of the new datatable entities to make the work with it easier - local/dto: data transfer objects that get transferred to the frontend - route/api: A complete REST api for a new frontend that can handle different requests to import a course or to save a course request for a teacher - route/schema: Only description on how the data looks like that is being transferred on the REST api --- classes/his_lsf.php | 92 -------- classes/local/cms/campus_management.php | 65 ++++++ classes/local/cms/his_lsf.php | 139 +++++++++++ classes/local/cms/sap.php | 53 +++++ classes/local/dto/course_dto.php | 113 +++++++++ .../models/cms.php} | 19 +- classes/local/models/course.php | 137 +++++++++++ classes/local/models/course_request.php | 126 ++++++++++ classes/local/models/imported_course.php | 78 +++++++ classes/local/service/cms_service.php | 149 ++++++++++++ classes/local/service/dashboard_service.php | 93 ++++++++ classes/route/api/dashboard_controller.php | 215 ++++++++++++++++++ classes/route/schema/category_schema.php | 65 ++++++ classes/route/schema/course_schema.php | 77 +++++++ .../schema/requestbodies/submit_body.php | 65 ++++++ .../dashboard_categories_response.php | 49 ++++ .../responses/dashboard_courses_response.php | 49 ++++ .../responses/dashboard_teachers_response.php | 49 ++++ .../schema/responses/submit_response.php | 55 +++++ classes/route/schema/teacher_schema.php | 66 ++++++ lang/en/local_lsf_unification.php | 57 +++++ lib.php | 5 + 22 files changed, 1711 insertions(+), 105 deletions(-) delete mode 100644 classes/his_lsf.php create mode 100644 classes/local/cms/campus_management.php create mode 100644 classes/local/cms/his_lsf.php create mode 100644 classes/local/cms/sap.php create mode 100644 classes/local/dto/course_dto.php rename classes/{campus_management.php => local/models/cms.php} (64%) create mode 100644 classes/local/models/course.php create mode 100644 classes/local/models/course_request.php create mode 100644 classes/local/models/imported_course.php create mode 100644 classes/local/service/cms_service.php create mode 100644 classes/local/service/dashboard_service.php create mode 100644 classes/route/api/dashboard_controller.php create mode 100644 classes/route/schema/category_schema.php create mode 100644 classes/route/schema/course_schema.php create mode 100644 classes/route/schema/requestbodies/submit_body.php create mode 100644 classes/route/schema/responses/dashboard_categories_response.php create mode 100644 classes/route/schema/responses/dashboard_courses_response.php create mode 100644 classes/route/schema/responses/dashboard_teachers_response.php create mode 100644 classes/route/schema/responses/submit_response.php create mode 100644 classes/route/schema/teacher_schema.php diff --git a/classes/his_lsf.php b/classes/his_lsf.php deleted file mode 100644 index 10fafce..0000000 --- a/classes/his_lsf.php +++ /dev/null @@ -1,92 +0,0 @@ -. - -namespace local_lsf_unification; - -use PDO; -use stdClass; - -/** - * Connection to the LSF database. - * - * @package local_lsf_unification - * @copyright 2026 Daniel Meißner - * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class his_lsf implements campus_management { - /** @var PDO The database connection to LSF */ - private PDO $db; - public function __construct( - /** @var stdClass The plugin configuration */ - private readonly stdClass $config - ) { - $this->db = new PDO($this->connection_string_from($config)); - } - - private function connection_string_from(stdClass $config): string { - $options = [ - 'host' => $config->dbhost, - 'port' => $config->dbport, - 'user' => $config->dbuser, - 'dbname' => $config->dbname, - ]; - - if ($config->dbusessl) { - $options['sslmode'] = 'verify-full'; - $options['sslrootcert'] = $config->dbsslrootcert; - $options['sslcert'] = $config->dbsslcert; - $options['sslkey'] = $config->dbsslkey; - } else { - $options['password'] = $config->dbpass; - } - - $parts = []; - foreach ($options as $key => $val) { - $escaped = addcslashes((string) $val, "\\'"); - $parts[] = "$key='$escaped'"; - } - return implode(' ', $parts); - } - - #[\Override] - public function courses_of_teacher(stdClass $teacher): array { - $courses = $this->db->query(" -SELECT - lv.veranstid, - lv.veranstnr, - lv.titel, - lv.semestertxt, - lv.urlveranst -FROM - learnweb_veranstaltung lv - JOIN learnweb_personal_veranst lpv ON lpv.veranstid = lv.veranstid - JOIN learnweb_personal lp ON lp.pid = lpv.pid -WHERE - lp.login = '{$teacher->username}' -", - PDO::FETCH_OBJ); - - return array_map( - function($course) { - $course->info = $course->titel . "  (" . - $course->semestertxt . - ' KVV-Nr. ' . $course->veranstnr . ')'; - return $course; - }, - $courses->fetchAll() - ); - } -} diff --git a/classes/local/cms/campus_management.php b/classes/local/cms/campus_management.php new file mode 100644 index 0000000..2e819bc --- /dev/null +++ b/classes/local/cms/campus_management.php @@ -0,0 +1,65 @@ +. + +namespace local_lsf_unification\local\cms; + +use moodle_url; + +/** + * Interface to campus management systems (CMS). + * + * @package local_lsf_unification + * @copyright 2026 Daniel Meißner + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface campus_management { + /** + * Query the CMS for a list of courses of a given teacher. + * + * @param object $teacher The user record of the teacher + * @return array A list of course objects + */ + public function get_courses_of_teacher(object $teacher): array; + + /** + * Query the CMS for all users that should be enrolled in the course with their respective role + * @param object $course + * @return array + */ + public function get_users_of_course(object $course): array; + + /** + * Add the link to the new existing moodle course to the course page in the cms. + * @param object $course + * @param moodle_url $url + * @return void + */ + public function set_moodle_link(object $course, moodle_url $url): void; + + /** + * Get all single dates on which the course occurs. Can be used to import dates in the moodle calendar. + * @param object $course + * @return array + */ + public function get_course_dates(object $course): array; + + /** + * Return all teachers registered in the cms that have courses in the configured timespan. Can be used to search for + * teachers when doing a course request. + * @return array + */ + public function get_teachers(): array; +} diff --git a/classes/local/cms/his_lsf.php b/classes/local/cms/his_lsf.php new file mode 100644 index 0000000..0571cd3 --- /dev/null +++ b/classes/local/cms/his_lsf.php @@ -0,0 +1,139 @@ +. + +namespace local_lsf_unification\local\cms; + +use DateTimeImmutable; +use DateTimeZone; +use Google\Service\Classroom\Teacher; +use local_lsf_unification\local\models\cms; +use local_lsf_unification\local\models\course; +use moodle_url; +use PDO; +use stdClass; + +/** + * Connection to the LSF database. + * + * @package local_lsf_unification + * @copyright 2026 Daniel Meißner + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class his_lsf implements campus_management { + /** @var PDO The database connection to LSF */ + private PDO $db; + + /** + * Constructor. + * @param stdClass $config The plugin configuration + */ + public function __construct(stdClass $config) { + $this->db = new PDO($this->connection_string_from($config)); + } + + /** + * Returns connection string. + * @param stdClass $config + * @return string + */ + private function connection_string_from(stdClass $config): string { + $options = ['host' => $config->dbhost, 'port' => $config->dbport, 'user' => $config->dbuser, 'dbname' => $config->dbname]; + + if (!empty($config->dbusessl)) { + $options['sslmode'] = 'verify-full'; + $options['sslrootcert'] = $config->dbsslrootcert; + $options['sslcert'] = $config->dbsslcert; + $options['sslkey'] = $config->dbsslkey; + } else { + $options['password'] = $config->dbpass; + } + + $parts = []; + foreach ($options as $key => $val) { + $escaped = addcslashes((string) $val, "\\'"); + $parts[] = "$key='$escaped'"; + } + return 'pgsql:' . implode(' ', $parts); + } + + #[\Override] + public function get_courses_of_teacher(object $teacher): array { + $maxage = get_config('local_lsf_unification', 'max_import_age'); + $rows = $this->db->query( + "SELECT lv.veranstid, lv.veranstnr, lv.titel, lv.semestertxt, lv.urlveranst, lv.veranstaltungsart, lv.zeitstempel, + COALESCE(lvk.description, '') AS description + FROM learnweb_veranstaltung lv + JOIN learnweb_personal_veranst lpv ON lpv.veranstid = lv.veranstid + JOIN learnweb_personal lp ON lp.pid = lpv.pid + LEFT JOIN LATERAL ( + SELECT string_agg(k.kommentar, '
' ORDER BY k.sprache) AS description + FROM learnweb_veranst_kommentar k + WHERE k.veranstid = lv.veranstid + ) lvk ON true + WHERE lp.login = '{$teacher->username}' + AND (CURRENT_DATE - CAST(lv.zeitstempel AS date)) < '{$maxage}' + ORDER BY semester, titel;", + PDO::FETCH_OBJ + ); + + return array_map( + fn($row) => new course( + cms: cms::LSF->value, + instance: (int) $row->veranstid, + title: $row->titel, + shorttitle: $row->veranstaltungsart, + description: $row->description, + teacher: $teacher->username, + semester: $row->semestertxt, + created: (new DateTimeImmutable($row->zeitstempel, new DateTimeZone('Europe/Berlin')))->getTimestamp(), + url: $row->urlveranst + ), + $rows->fetchAll() + ); + } + + #[\Override] + public function get_users_of_course(object $course): array { + return []; + } + + #[\Override] + public function set_moodle_link(object $course, moodle_url $url): void { + return; + } + + #[\Override] + public function get_course_dates(object $course): array { + return []; + } + + #[\Override] + public function get_teachers(): array { + $maxage = get_config('local_lsf_unification', 'max_import_age'); + $rows = $this->db->query( + "SELECT DISTINCT lp.zivk, lp.vorname, lp.nachname + FROM learnweb_personal lp + JOIN learnweb_personal_veranst lpv ON lpv.pid = lp.pid + JOIN learnweb_veranstaltung lv ON lv.veranstid = lpv.veranstid + WHERE (CURRENT_DATE - CAST(lv.zeitstempel AS date)) < '{$maxage}';", + PDO::FETCH_OBJ + ); + return array_map( + fn($row) => (object) ['username' => $row->zivk, 'firstname' => $row->vorname, 'lastname' => $row->nachname], + $rows->fetchAll() + ); + } +} diff --git a/classes/local/cms/sap.php b/classes/local/cms/sap.php new file mode 100644 index 0000000..70a9e79 --- /dev/null +++ b/classes/local/cms/sap.php @@ -0,0 +1,53 @@ +. + +namespace local_lsf_unification\local\cms; + +use moodle_url; + +/** + * Connection to the SAP database. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sap implements campus_management { + #[\Override] + public function get_courses_of_teacher(object $teacher): array { + return []; + } + + #[\Override] + public function get_users_of_course(object $course): array { + return []; + } + + #[\Override] + public function set_moodle_link(object $course, moodle_url $url): void { + return; + } + + #[\Override] + public function get_course_dates(object $course): array { + return []; + } + + #[\Override] + public function get_teachers(): array { + return []; + } +} diff --git a/classes/local/dto/course_dto.php b/classes/local/dto/course_dto.php new file mode 100644 index 0000000..b98880c --- /dev/null +++ b/classes/local/dto/course_dto.php @@ -0,0 +1,113 @@ +. + +namespace local_lsf_unification\local\dto; + +use local_lsf_unification\local\models\course; +use moodle_url; + +/** + * DTO for a course that gets shown in the user dashboard. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_dto { + /** + * Constructor. + */ + public function __construct( + /** @var int unique id of the abstracted course */ + public readonly int $id, + /** @var string cms the course originates from */ + public readonly string $cms, + /** @var int id of the course in the cms */ + public readonly int $cmsinstance, + /** @var string url to the course in the cms */ + public readonly string $cmsurl, + /** @var string title of the course */ + public readonly string $title, + /** @var string short title of the course */ + public readonly string $shorttitle, + /** @var string description of the course */ + public readonly string $description, + /** @var string user name of the main responsible teacher */ + public readonly string $teacher, + /** @var string semester the course belongs to */ + public readonly string $semester, + /** @var int timestamp of when the course was created in the cms */ + public readonly int $created, + /** @var int abstract course id */ + public readonly int $courseid, + /** @var int moodle course id, 0 when not yet created */ + public readonly int $moodleid = 0, + /** @var int request state of the user, if there is no request then the state is 0 */ + public readonly int $requeststate = 0, + /** @var string url to the course in moodle */ + public readonly string $moodleurl = '', + ) { + } + + /** + * Builds a course dto from a course entity. + * + * @param course $course + * @return self + */ + public static function from_course(course $course, int $moodleid = 0, int $requeststate = 0): self { + return new self( + $course->id, + $course->cms, + $course->instance, + $course->url, + $course->title, + $course->shorttitle, + $course->description, + $course->teacher, + $course->semester, + $course->created, + $course->id, + $moodleid, + $requeststate, + $moodleid != 0 ? (new moodle_url('/course/view.php', ['id' => $moodleid]))->out() : '', + ); + } + + /** + * Returns the wire format (JSON shape) of this dashboard course. + * + * @return array + */ + public function to_array(): array { + return [ + 'id' => $this->id, + 'cms' => $this->cms, + 'cmsinstance' => $this->cmsinstance, + 'cmsurl' => $this->cmsurl, + 'title' => $this->title, + 'shorttitle' => $this->shorttitle, + 'description' => $this->description, + 'teacher' => $this->teacher, + 'semester' => $this->semester, + 'created' => $this->created, + 'courseid' => $this->courseid, + 'moodleid' => $this->moodleid, + 'requeststate' => $this->requeststate, + 'moodleurl' => $this->moodleurl, + ]; + } +} diff --git a/classes/campus_management.php b/classes/local/models/cms.php similarity index 64% rename from classes/campus_management.php rename to classes/local/models/cms.php index 1336a9b..f2ac464 100644 --- a/classes/campus_management.php +++ b/classes/local/models/cms.php @@ -14,23 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace local_lsf_unification; - -use stdClass; +namespace local_lsf_unification\local\models; /** - * Interface to campus management systems (CMS). + * Declares all existing content management system that are used in lsf_unification. * * @package local_lsf_unification - * @copyright 2026 Daniel Meißner + * @copyright 2026 Tamaro Walter * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -interface campus_management { - /** - * Query the CMS for a list of courses of a given teacher. - * - * @param stdClass $teacher The user record of the teacher - * @return array A list of course objects - */ - public function courses_of_teacher(stdClass $teacher): array; +enum cms: string { + case LSF = 'lsf'; + case SAP = 'sap'; } diff --git a/classes/local/models/course.php b/classes/local/models/course.php new file mode 100644 index 0000000..e4a4f8c --- /dev/null +++ b/classes/local/models/course.php @@ -0,0 +1,137 @@ +. + +namespace local_lsf_unification\local\models; + +use dml_exception; +use local_lsf_unification\local\cms\campus_management; + +/** + * This class represents an abstracted course from any cms. Is used as preparation to create a moodle course. This class is not + * Moodle course itself. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course { + /** @var string Name of the entities table in the moodle database */ + const DB_TABLE_NAME = "local_lsf_unification_courses"; + + /** @var int unique ID in course table */ + public readonly int $id; + + /** @var array all users of the course including their role */ + private array $users; + + /** @var array timestamps of all dates the course have */ + private array $dates; + + /** + * Course constructor. This either returns an existing course or create a new one when no ID is passed. If you already have the + * course please use the static from_record() function to work with courses. + * @throws dml_exception + */ + public function __construct( + /** @var string which cms the course originates from */ + public readonly string $cms, + /** @var int id of the course in the cms */ + public readonly int $instance, + /** @var string title of the course */ + public readonly string $title, + /** @var string short title of the course */ + public readonly string $shorttitle, + /** @var string description of the course */ + public readonly string $description, + /** @var string user name of the main responsible teacher of the course */ + public readonly string $teacher, + /** @var string which semester the course belongs to */ + public readonly string $semester, + /** @var int timestamp of when the course was created in the cms */ + public readonly int $created, + /** @var string url to the course in the cms */ + public readonly string $url, + ?int $id = null, + ) { + global $DB; + if ($id !== null) { + $this->id = $id; + } else { + $existing = $DB->get_record(self::DB_TABLE_NAME, ['cms' => $this->cms, 'instance' => $this->instance]); + $this->id = $existing->id ?? $DB->insert_record(self::DB_TABLE_NAME, (object) [ + 'cms' => $this->cms, + 'instance' => $this->instance, + 'title' => $this->title, + 'shorttitle' => $this->shorttitle, + 'description' => $this->description, + 'teacher' => $this->teacher, + 'semester' => $this->semester, + 'created' => $this->created, + 'url' => $this->url, + ]); + } + } + + /** + * Returns a course from a course id. + * @param int $id + * @return course + */ + public static function construct_from_id(int $id): course { + global $DB; + return self::construct_from_record($DB->get_record(self::DB_TABLE_NAME, ['id' => $id], '*', MUST_EXIST)); + } + + /** + * Returns a course from a course id. + * @param int $id + * @return course + */ + public static function construct_from_record(object $record): course { + return new self( + $record->cms, + $record->instance, + $record->title, + $record->shorttitle, + $record->description, + $record->teacher, + $record->semester, + $record->created, + $record->url, + $record->id, + ); + } + + /** + * Loads and caches the users of this course from the given campus management system. + * + * @param campus_management $cms + * @return void + */ + public function load_users_of_course(campus_management $cms): void { + $this->users = $cms->get_users_of_course($this); + } + + /** + * Loads and caches the dates of this course from the given campus management system. + * + * @param campus_management $cms + * @return void + */ + public function load_dates_of_course(campus_management $cms): void { + $this->dates = $cms->get_course_dates($this); + } +} diff --git a/classes/local/models/course_request.php b/classes/local/models/course_request.php new file mode 100644 index 0000000..adfa3dc --- /dev/null +++ b/classes/local/models/course_request.php @@ -0,0 +1,126 @@ +. + +namespace local_lsf_unification\local\models; + +use coding_exception; +use dml_exception; + +/** + * This class represents pending request for a course, that was openend when a non teacher tried to import a course from a cms. + * An object of this class represents an entity in the course_request table + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_request { + /** @var string Name of the entities table in the moodle database */ + const DB_TABLE_NAME = "local_lsf_unification_course_requests"; + + /** Pending status. */ + const REQUEST_PENDING = 1; + + /** Accepted status.*/ + const REQUEST_ACCEPTED = 2; + + /** Declined status. */ + const REQUEST_DECLINED = 3; + + /** Imported status. The course was imported by the teacher after the request. */ + const REQUEST_IMPORTED = 4; + + /** @var int Unique ID in the database table*/ + public readonly int $id; + + /** + * .Constructor + * @throws dml_exception + */ + public function __construct( + /** @var int moodle id of the user that requested the course */ + public readonly int $requesterid, + /** @var int abstract course id (not a moodle course id!) */ + public readonly int $courseid, + /** @var int current state of the request */ + private int $state, + ?int $id = null, + ) { + global $DB; + + $this->id = $id ?? $DB->insert_record(self::DB_TABLE_NAME, (object) [ + 'requesterid' => $this->requesterid, + 'courseid' => $this->courseid, + 'state' => $this->state, + ]); + } + + /** + * Returns a course reqeust from an id. + * @param int $id + * @return course_request + */ + public static function construct_from_id(int $id): course_request { + global $DB; + return self::construct_from_record($DB->get_record(self::DB_TABLE_NAME, ['id' => $id], '*', MUST_EXIST)); + } + + /** + * Returns a course reqeust from a record. + * @param object $record + * @return course_request + * @throws coding_exception + */ + public static function construct_from_record(object $record): course_request { + return new self($record->requesterid, $record->courseid, $record->state, $record->id); + } + + /** + * Approves the request + * @return void + */ + public function approve(): void { + global $DB; + if ($this->state == self::REQUEST_PENDING) { + $this->state = self::REQUEST_ACCEPTED; + $DB->update_record(self::DB_TABLE_NAME, $this->get_db_object()); + } + } + + /** + * Declines the request + * @return void + */ + public function decline(): void { + global $DB; + if ($this->state == self::REQUEST_PENDING) { + $this->state = self::REQUEST_DECLINED; + $DB->update_record(self::DB_TABLE_NAME, $this->get_db_object()); + } + } + + /** + * Returns a plain database object representation of this request. + * @return object + */ + private function get_db_object(): object { + return (object) [ + 'id' => $this->id, + 'requesterid' => $this->requesterid, + 'courseid' => $this->courseid, + 'state' => $this->state, + ]; + } +} diff --git a/classes/local/models/imported_course.php b/classes/local/models/imported_course.php new file mode 100644 index 0000000..2d11062 --- /dev/null +++ b/classes/local/models/imported_course.php @@ -0,0 +1,78 @@ +. + +namespace local_lsf_unification\local\models; + +use dml_exception; + +/** + * This class represents an imported course to the moodle system. + * An object of this class represents an entity in the database table + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class imported_course { + /** @var string Name of the entities table in the moodle database */ + const DB_TABLE_NAME = "local_lsf_unification_imported_courses"; + + /** @var int Unique identifier */ + public readonly int $id; + + /** + * Constructor. creates db entity on the fly if no id is passed. + * @param int $moodleid + * @param int $courseid + * @param int|null $id + * @throws dml_exception + */ + public function __construct( + /** @var int Moodle course id */ + public readonly int $moodleid, + /** @var int abstract course id */ + public readonly int $courseid, + ?int $id = null, + ) { + global $DB; + $this->id = $id ?? $DB->insert_record(self::DB_TABLE_NAME, (object) [ + 'moodleid' => $this->moodleid, + 'courseid' => $this->courseid, + ]); + } + + /** + * Builds object from row id. + * @param int $id + * @return imported_course + * @throws dml_exception + */ + public static function construct_from_id(int $id): imported_course { + global $DB; + return self::construct_from_record( + $DB->get_record(self::DB_TABLE_NAME, ['id' => $id], '*', MUST_EXIST) + ); + } + + /** + * Buids object from db record. + * @param object $record + * @return imported_course + * @throws dml_exception + */ + public static function construct_from_record(object $record): imported_course { + return new self($record->moodleid, $record->courseid, $record->id); + } +} diff --git a/classes/local/service/cms_service.php b/classes/local/service/cms_service.php new file mode 100644 index 0000000..7493aee --- /dev/null +++ b/classes/local/service/cms_service.php @@ -0,0 +1,149 @@ +. + +namespace local_lsf_unification\local\service; + +use Exception; +use local_lsf_unification\local\cms\his_lsf; +use local_lsf_unification\local\cms\sap; +use local_lsf_unification\local\dto\course_dto; +use local_lsf_unification\local\models\course; +use local_lsf_unification\local\models\course_request; +use local_lsf_unification\local\models\imported_course; + +/** + * Service class that handles requests that concerns the cms classes. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cms_service { + /** + * Returns every course the teacher has in every cms as an array of course_dto dto's. + * @param ?object $teacher You can pass a certain teacher, otherwise $USER is used. + * @param bool $isrequest If the call comes from a request on behalf of a teacher. + * @return array + */ + public static function get_courses_from_teacher(object $teacher = null, bool $isrequest = false): array { + global $USER, $DB; + $teacher = $teacher ?? $USER; + $lsf = new his_lsf(get_config('local_lsf_unification')); + $sap = new sap(); + $courses = array_merge($lsf->get_courses_of_teacher($teacher), $sap->get_courses_of_teacher($teacher)); + + // Filter out already imported courses and courses that the current user requested. A user can not request a course twice. + $importedids = $DB->get_fieldset(imported_course::DB_TABLE_NAME, 'courseid'); + $requestids = $isrequest ? $DB->get_fieldset(course_request::DB_TABLE_NAME, 'courseid', ['requesterid' => $USER->id]) : []; + + // Combine the ids and then flip keys and values to make an array lookup instead of value scanning. + $excluded = array_flip(array_merge($importedids, $requestids)); + $courses = array_values(array_filter($courses, fn(course $course) => !isset($excluded[$course->id]))); + + // Create the DTO's. + return array_map( + fn(course $course) => course_dto::from_course($course), + $courses, + ); + } + + /** + * Return an array of teachers that have courses in the cms. + * @return array + */ + public static function get_teachers(): array { + $lsf = new his_lsf(get_config('local_lsf_unification')); + $sap = new sap(); + // LEARNWEB-TODO: Keep in mind that the same teacher can be in sap and lsf (but they can have different username). + return array_merge($lsf->get_teachers(), $sap->get_teachers()); + } + + /** + * Saves a course request in the database. + * @return bool + */ + public static function add_request(object $course): bool { + global $DB, $USER; + // 1. Step: Validations: Check that the course really exists. + if (empty($DB->get_record(course::DB_TABLE_NAME, ["id" => $course->id]))) { + return false; + } + // LEARNWEB-TODO: What happens if there is already a request for that course? I think there could be many pending requests. + // LEARNWEB-TODO: but if a techer accepts a requests all pending ones get declined. On decline only the one gets deleted. + + // 2. Step: Create a request entry. The requester is the current user. + $request = (object) ['requesterid' => $USER->id, 'courseid' => $course->id, 'state' => course_request::REQUEST_PENDING]; + $result = (bool) $DB->insert_record(course_request::DB_TABLE_NAME, $request); + + // 3. Sent an email. + return $result; + } + + /** + * Imports a cms course into Moodle: creates the real Moodle course, enrols the importing user + * as its teacher and records that the cms course now has a Moodle counterpart. + * + * @param object $course The course the wizard submitted, in the shape of a course_dto. + * @param object $category The Moodle category the course is created in. + * @return bool Whether the import went through. + */ + public static function import_course(object $course, object $category): bool { + global $CFG, $DB, $USER; + require_once($CFG->dirroot . '/course/lib.php'); + + // 1. Step: Validations: Check that the course really exists. Nothing is created if it does not. + if (empty($DB->get_record(course::DB_TABLE_NAME, ["id" => $course->id]))) { + return false; + } + + try { + $transaction = $DB->start_delegated_transaction(); + + // Create a unique shortname. + $shortname = "{$course->shorttitle}_{$course->semester}"; + if ($DB->record_exists('course', ['shortname' => $shortname])) { + $shortname .= "_{$course->cmsinstance}"; + } + + // 2. Create the moodle course. + $moodlecourse = create_course((object) [ + 'category' => $category->id, + 'fullname' => $course->title, + 'shortname' => $shortname, + 'idnumber' => "{$course->cms}_{$course->cmsinstance}", + 'summary' => $course->description, + 'summaryformat' => FORMAT_HTML, + 'startdate' => $course->created, + ]); + + // 3. Step: Enrol the teacher into the course. + $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher'], MUST_EXIST); + enrol_try_internal_enrol($moodlecourse->id, $USER->id, $roleid); + + // 4. Mark all requests to this course as "imported". + $DB->set_field(course_request::DB_TABLE_NAME, 'state', course_request::REQUEST_IMPORTED, ['courseid' => $course->id]); + + // 5. Step. Create a course import entry. + $import = new imported_course($moodlecourse->id, $course->id); + + $transaction->allow_commit(); + return !empty($import->id); + } catch (Exception $e) { + $transaction->rollback($e); + } + return false; + } +} diff --git a/classes/local/service/dashboard_service.php b/classes/local/service/dashboard_service.php new file mode 100644 index 0000000..782fce7 --- /dev/null +++ b/classes/local/service/dashboard_service.php @@ -0,0 +1,93 @@ +. + +namespace local_lsf_unification\local\service; + +use local_lsf_unification\local\dto\course_dto; +use moodle_url; + +/** + * Service class that handles requests of the dashboard that are not cms specific. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dashboard_service { + /** + * Returns all imported or requested courses from the user that are shown on the dashboard. + * @return array + */ + public static function get_dashboard_courses(): array { + global $DB, $USER; + $mintime = time() - (get_config('local_lsf_unification', 'max_import_age') * DAYSECS); + // Get Requested courses. + $sql = "SELECT request.id AS requestid, course.*, request.state AS state + FROM {local_lsf_unification_course_requests} request + JOIN {local_lsf_unification_courses} course ON course.id = request.courseid + WHERE request.requesterid = :userid + AND course.created > :mintime + ORDER BY course.created DESC"; + $requests = $DB->get_records_sql($sql, ['userid' => $USER->id, 'mintime' => $mintime]); + + // Get imported courses. + $sql = "SELECT course.*, import.moodleid AS moodleid + FROM {local_lsf_unification_imported_courses} import + JOIN {local_lsf_unification_courses} course ON course.id = import.courseid + WHERE course.teacher = :username + AND course.created > :mintime + ORDER BY course.created DESC"; + $imported = $DB->get_records_sql($sql, ['username' => $USER->username, 'mintime' => $mintime]); + $courses = array_merge($requests, $imported); + + // Return course dto and set the moodle id or state if its set. + return array_map( + fn(int $i, object $course) => new course_dto( + $i, + $course->cms, + $course->instance, + $course->url, + $course->title, + $course->shorttitle, + '', + $course->teacher, + $course->semester, + $course->created, + $course->id, + $course->moodleid ?? 0, + $course->state ?? 0, + !empty($course->moodleid) ? (new moodle_url('/course/view.php', ['id' => $course->moodleid]))->out() : '', + ), + array_keys($courses), + $courses, + ); + } + + /** + * Returns all existing Moodle course categories as a flat list. Each entry has the + * category id and its display name (the full path, e.g. "Faculty / Institute"), which + * is what the wizard shows in the category dropdown. + * @return array + */ + public static function get_categories(): array { + $list = \core_course_category::make_categories_list(); + return array_map( + fn(int $id, string $name) => ['id' => $id, 'name' => $name], + array_keys($list), + $list, + ); + } +} diff --git a/classes/route/api/dashboard_controller.php b/classes/route/api/dashboard_controller.php new file mode 100644 index 0000000..e68a939 --- /dev/null +++ b/classes/route/api/dashboard_controller.php @@ -0,0 +1,215 @@ +. + +/** + * REST API controller for the LSF unification dashboard. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_lsf_unification\route\api; + +use core\exception\invalid_parameter_exception; +use core\param; +use core\router\route; +use core\router\schema\response\payload_response; +use core\router\schema\parameters\path_parameter; +use local_lsf_unification\local\dto\course_dto; +use local_lsf_unification\local\service\cms_service; +use local_lsf_unification\local\service\dashboard_service; +use local_lsf_unification\route\schema\requestbodies\submit_body; +use local_lsf_unification\route\schema\responses\dashboard_categories_response; +use local_lsf_unification\route\schema\responses\dashboard_courses_response; +use local_lsf_unification\route\schema\responses\dashboard_teachers_response; +use local_lsf_unification\route\schema\responses\submit_response; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Endpoint: GET /api/rest/v2/local_lsf_unification/dashboard/courses + */ +class dashboard_controller { + /** + * Return all dashboard (imported and requested) courses of the current user. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + */ + #[route( + title: 'Get Dashboard courses', + description: "Returns the current users imported and requested courses", + path: '/dashboard/dashboardcourses', + method: ['GET'], + responses: [new dashboard_courses_response()], + )] + public function get_dashboard_courses(ServerRequestInterface $request, ResponseInterface $response): payload_response { + $payload = array_map( + fn(course_dto $dto) => $dto->to_array(), + dashboard_service::get_dashboard_courses() + ); + return new payload_response($payload, $request, $response); + } + + /** + * Return all cms courses of the current user. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + */ + #[route( + title: 'Get cms courses', + description: "Returns the current user's cms courses.", + path: '/dashboard/courses', + method: ['GET'], + responses: [new dashboard_courses_response()], + )] + public function get_courses(ServerRequestInterface $request, ResponseInterface $response): payload_response { + $payload = array_map( + fn(course_dto $dto) => $dto->to_array(), + cms_service::get_courses_from_teacher() + ); + return new payload_response($payload, $request, $response); + } + + /** + * Return all cms courses of a specific user. + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param string $username + * @return payload_response + */ + #[route( + title: 'Get cms courses for a user', + description: "Returns a given user's cms courses.", + path: '/dashboard/courses/{username}', + method: ['GET'], + pathtypes: [new path_parameter(name: 'username', type: param::ALPHANUM)], + responses: [new dashboard_courses_response()], + )] + public function get_courses_for_user( + ServerRequestInterface $request, + ResponseInterface $response, + string $username + ): payload_response { + $payload = array_map( + fn(course_dto $dto) => $dto->to_array(), + cms_service::get_courses_from_teacher((object) ['username' => $username], true) + ); + return new payload_response($payload, $request, $response); + } + + /** + * Return all teachers with current courses in the cms. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + */ + #[route( + title: 'Get teachers', + description: "Returns a list of teachers that have courses.", + path: '/dashboard/teachers', + method: ['GET'], + responses: [new dashboard_teachers_response()], + )] + public function get_teachers(ServerRequestInterface $request, ResponseInterface $response): payload_response { + return new payload_response(cms_service::get_teachers(), $request, $response); + } + + /** + * Return all existing Moodle course categories. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + */ + #[route( + title: 'Get categories', + description: "Returns all existing Moodle course categories the teacher can import into.", + path: '/dashboard/categories', + method: ['GET'], + responses: [new dashboard_categories_response()], + )] + public function get_categories(ServerRequestInterface $request, ResponseInterface $response): payload_response { + return new payload_response(dashboard_service::get_categories(), $request, $response); + } + + /** + * Save a course request that was built in the wizard. + * + * The body is the wizard cache: the branch plus the chosen teacher and course. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + * @throws invalid_parameter_exception If the body is missing the branch, teacher or course. + */ + #[route( + title: 'Save a course request', + description: 'Stores a course request that was built in the wizard.', + path: '/dashboard/request', + method: ['POST'], + requestbody: new submit_body(), + responses: [new submit_response()], + )] + public function post_request(ServerRequestInterface $request, ResponseInterface $response): payload_response { + $cache = $request->getParsedBody(); + + // The schema hands a slot the other branch owns through as null, so check here that the + // keys this branch actually needs were filled in. + foreach (['branch', 'teacher', 'course'] as $key) { + if (empty($cache[$key])) { + throw new invalid_parameter_exception("Missing '{$key}' in the request body."); + } + } + $status = cms_service::add_request((object) $cache['course']); + return new payload_response(['status' => $status], $request, $response); + } + + /** + * Save a course import. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + * @throws invalid_parameter_exception If the body is missing the branch, teacher or course. + */ + #[route( + title: 'Save a course import', + description: 'Stores a course import that was built in the wizard.', + path: '/dashboard/import', + method: ['POST'], + requestbody: new submit_body(), + responses: [new submit_response()], + )] + public function post_import(ServerRequestInterface $request, ResponseInterface $response): payload_response { + $cache = $request->getParsedBody(); + + // The schema hands a slot the other branch owns through as null, so check here that the + // keys this branch actually needs were filled in. + foreach (['branch', 'course', 'category'] as $key) { + if (empty($cache[$key])) { + throw new invalid_parameter_exception("Missing '{$key}' in the request body."); + } + } + $status = cms_service::import_course((object) $cache['course'], (object) $cache['category']); + return new payload_response(['status' => $status], $request, $response); + } +} diff --git a/classes/route/schema/category_schema.php b/classes/route/schema/category_schema.php new file mode 100644 index 0000000..a05b4c5 --- /dev/null +++ b/classes/route/schema/category_schema.php @@ -0,0 +1,65 @@ +. + +namespace local_lsf_unification\route\schema; + +use core\param; +use core\router\schema\objects\schema_object; +use core\router\schema\objects\scalar_type; + +/** + * Schema of a single Moodle course category. + * + * Only the id and the display name are exposed; the id is what the wizard stores + * and submits, the name is what the teacher sees in the dropdown. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class category_schema extends schema_object { + /** + * Constructor. + * + * @param bool $required Whether every field must be non-null. Only has an effect on + * request bodies; responses are never validated against their schema. + * @throws \core\exception\coding_exception + */ + public function __construct(bool $required = false) { + parent::__construct( + content: [ + 'id' => new scalar_type(param::INT, required: $required), + 'name' => new scalar_type(param::RAW, required: $required), + ], + ); + } + + /** + * Validate the data against the schema. + * + * The wizard submits its whole cache, so the slots the current branch does not fill arrive + * as null. A schema_object cannot express "nullable" the way a scalar_type can and iterates + * the value unguarded, so a null is caught here and handed on untouched. The controller is + * what decides whether the branch it got actually needed this key. + * + * @param mixed $data + * @return mixed + */ + #[\Override] + public function validate_data(mixed $data) { + return $data === null ? null : parent::validate_data($data); + } +} diff --git a/classes/route/schema/course_schema.php b/classes/route/schema/course_schema.php new file mode 100644 index 0000000..da44b5a --- /dev/null +++ b/classes/route/schema/course_schema.php @@ -0,0 +1,77 @@ +. + +namespace local_lsf_unification\route\schema; + +use core\param; +use core\router\schema\objects\schema_object; +use core\router\schema\objects\scalar_type; + +/** + * Schema of a single course. Must be equal to the course_dto. + * + * Shared by the responses that return courses and by the request bodies that accept one, + * so that a change to the dto only has to be made here. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_schema extends schema_object { + /** + * Constructor. + * + * @param bool $required Whether every field must be non-null. Only has an effect on + * request bodies; responses are never validated against their schema. + * @throws \core\exception\coding_exception + */ + public function __construct(bool $required = false) { + parent::__construct( + content: [ + 'id' => new scalar_type(param::INT, required: $required), + 'cms' => new scalar_type(param::RAW, required: $required), + 'cmsinstance' => new scalar_type(param::INT, required: $required), + 'cmsurl' => new scalar_type(param::RAW, required: $required), + 'title' => new scalar_type(param::RAW, required: $required), + 'shorttitle' => new scalar_type(param::RAW, required: $required), + 'description' => new scalar_type(param::RAW, required: $required), + 'teacher' => new scalar_type(param::RAW, required: $required), + 'semester' => new scalar_type(param::RAW, required: $required), + 'created' => new scalar_type(param::INT, required: $required), + 'state' => new scalar_type(param::INT, required: $required), + 'courseid' => new scalar_type(param::INT, required: $required), + 'moodleid' => new scalar_type(param::INT, required: $required), + 'requeststate' => new scalar_type(param::INT, required: $required), + ], + ); + } + + /** + * Validate the data against the schema. + * + * The wizard submits its whole cache, so the slots the current branch does not fill arrive + * as null. A schema_object cannot express "nullable" the way a scalar_type can and iterates + * the value unguarded, so a null is caught here and handed on untouched. The controller is + * what decides whether the branch it got actually needed this key. + * + * @param mixed $data + * @return mixed + */ + #[\Override] + public function validate_data(mixed $data) { + return $data === null ? null : parent::validate_data($data); + } +} diff --git a/classes/route/schema/requestbodies/submit_body.php b/classes/route/schema/requestbodies/submit_body.php new file mode 100644 index 0000000..4de4c69 --- /dev/null +++ b/classes/route/schema/requestbodies/submit_body.php @@ -0,0 +1,65 @@ +. + +namespace local_lsf_unification\route\schema\requestbodies; + +use core\param; +use core\router\schema\objects\schema_object; +use core\router\schema\objects\scalar_type; +use core\router\schema\request_body; +use core\router\schema\response\content\payload_response_type; +use local_lsf_unification\route\schema\category_schema; +use local_lsf_unification\route\schema\course_schema; +use local_lsf_unification\route\schema\teacher_schema; + +/** + * Request body specification of the wizard cache that is sent when the wizard is submitted. + * + * Shared by both branches: the wizard always submits its whole cache, so the request submit + * sends a teacher and a null category and the import submit sends a category and a null + * teacher. Only the branch and the course are common to both, so the teacher and the category + * are declared optional here and the controller checks per branch which of them it needs. + * + * The schema doubles as an allowlist: any key that is not declared here is stripped + * from the parsed body before the controller sees it. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class submit_body extends request_body { + /** + * Constructor. + * @throws \core\exception\coding_exception + */ + public function __construct() { + parent::__construct( + description: 'The wizard cache: the branch the user took plus the chosen course, ' . + 'the teacher (request branch) and the category (import branch).', + content: new payload_response_type( + schema: new schema_object( + content: [ + 'branch' => new scalar_type(param::ALPHA, required: true), + 'teacher' => new teacher_schema(), + 'course' => new course_schema(required: true), + 'category' => new category_schema(), + ], + ), + ), + required: true, + ); + } +} diff --git a/classes/route/schema/responses/dashboard_categories_response.php b/classes/route/schema/responses/dashboard_categories_response.php new file mode 100644 index 0000000..2fe1397 --- /dev/null +++ b/classes/route/schema/responses/dashboard_categories_response.php @@ -0,0 +1,49 @@ +. + +namespace local_lsf_unification\route\schema\responses; + +use core\router\schema\objects\array_of_things; +use core\router\schema\response\content\json_media_type; +use core\router\schema\response\response; +use local_lsf_unification\route\schema\category_schema; + +/** + * Response specification of the array of Moodle categories the teacher can pick from in the wizard. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dashboard_categories_response extends response { + /** + * Constructor. + * @throws \core\exception\coding_exception + */ + public function __construct() { + parent::__construct( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new array_of_things( + thingtype: new category_schema(), + ), + ), + ], + ); + } +} diff --git a/classes/route/schema/responses/dashboard_courses_response.php b/classes/route/schema/responses/dashboard_courses_response.php new file mode 100644 index 0000000..43443b0 --- /dev/null +++ b/classes/route/schema/responses/dashboard_courses_response.php @@ -0,0 +1,49 @@ +. + +namespace local_lsf_unification\route\schema\responses; + +use core\router\schema\objects\array_of_things; +use core\router\schema\response\content\json_media_type; +use core\router\schema\response\response; +use local_lsf_unification\route\schema\course_schema; + +/** + * Response specification of the array of courses that are shown in the dashboard. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dashboard_courses_response extends response { + /** + * Constructor. + * @throws \core\exception\coding_exception + */ + public function __construct() { + parent::__construct( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new array_of_things( + thingtype: new course_schema(), + ), + ), + ], + ); + } +} diff --git a/classes/route/schema/responses/dashboard_teachers_response.php b/classes/route/schema/responses/dashboard_teachers_response.php new file mode 100644 index 0000000..4748e56 --- /dev/null +++ b/classes/route/schema/responses/dashboard_teachers_response.php @@ -0,0 +1,49 @@ +. + +namespace local_lsf_unification\route\schema\responses; + +use core\router\schema\objects\array_of_things; +use core\router\schema\response\content\json_media_type; +use core\router\schema\response\response; +use local_lsf_unification\route\schema\teacher_schema; + +/** + * Response specification of the array of teacher users that are shown in the request page. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dashboard_teachers_response extends response { + /** + * Constructor. + * @throws \core\exception\coding_exception + */ + public function __construct() { + parent::__construct( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new array_of_things( + thingtype: new teacher_schema(), + ), + ), + ], + ); + } +} diff --git a/classes/route/schema/responses/submit_response.php b/classes/route/schema/responses/submit_response.php new file mode 100644 index 0000000..f39fdb4 --- /dev/null +++ b/classes/route/schema/responses/submit_response.php @@ -0,0 +1,55 @@ +. + +namespace local_lsf_unification\route\schema\responses; + +use core\param; +use core\router\schema\objects\schema_object; +use core\router\schema\objects\scalar_type; +use core\router\schema\response\content\json_media_type; +use core\router\schema\response\response; + +/** + * Response specification returned after the wizard was submitted. + * + * Shared by both branches: saving a course request and importing a course both only + * report whether they succeeded. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class submit_response extends response { + /** + * Constructor. + * @throws \core\exception\coding_exception + */ + public function __construct() { + parent::__construct( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new schema_object( + content: [ + 'status' => new scalar_type(param::BOOL), + ], + ), + ), + ], + ); + } +} diff --git a/classes/route/schema/teacher_schema.php b/classes/route/schema/teacher_schema.php new file mode 100644 index 0000000..931cf23 --- /dev/null +++ b/classes/route/schema/teacher_schema.php @@ -0,0 +1,66 @@ +. + +namespace local_lsf_unification\route\schema; + +use core\param; +use core\router\schema\objects\schema_object; +use core\router\schema\objects\scalar_type; + +/** + * Schema of a single teacher. + * + * Shared by the responses that return teachers and by the request bodies that accept one, + * so that a change only has to be made here. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class teacher_schema extends schema_object { + /** + * Constructor. + * + * @param bool $required Whether every field must be non-null. Only has an effect on + * request bodies; responses are never validated against their schema. + * @throws \core\exception\coding_exception + */ + public function __construct(bool $required = false) { + parent::__construct( + content: [ + 'username' => new scalar_type(param::USERNAME, required: $required), + 'firstname' => new scalar_type(param::RAW, required: $required), + 'lastname' => new scalar_type(param::RAW, required: $required), + ], + ); + } + + /** + * Validate the data against the schema. + * + * The wizard submits its whole cache, so the slots the current branch does not fill arrive + * as null. A schema_object cannot express "nullable" the way a scalar_type can and iterates + * the value unguarded, so a null is caught here and handed on untouched. The controller is + * what decides whether the branch it got actually needed this key. + * + * @param mixed $data + * @return mixed + */ + #[\Override] + public function validate_data(mixed $data) { + return $data === null ? null : parent::validate_data($data); + } +} diff --git a/lang/en/local_lsf_unification.php b/lang/en/local_lsf_unification.php index ebc807e..a28a00d 100644 --- a/lang/en/local_lsf_unification.php +++ b/lang/en/local_lsf_unification.php @@ -34,9 +34,16 @@ $string['answer_proxy_creation'] = "The course exists on the HIS-LSF platform and you are authorized to create this course on behalf of a registered teacher."; $string['answer_sent'] = 'Thank you for processing this request, your decision will be sent to the requester.'; $string['back'] = 'Back'; +$string['category'] = 'Category'; +$string['category_choose'] = 'Choose a category ...'; +$string['category_validate'] = 'Please choose a category.'; $string['categoryinvalid'] = 'please choose a category from this selection'; +$string['choose_action'] = 'What do you want to do?'; +$string['choose_action_import'] = 'Import an existing course from the HIS-LSF or SAP platform to moodle.'; +$string['choose_action_request'] = 'Request a course on behalf of a teacher.'; $string['choose_course'] = 'Please select the course you want to create.'; $string['choose_teacher'] = 'Please enter the username of the authorizing teacher:'; +$string['close'] = 'Close'; $string['config_auto_update'] = "Auto-Update from His-Lsf"; $string['config_auto_update_duration'] = "Keep newly assigned users up to date for"; $string['config_auto_update_duration-1'] = "never"; @@ -61,7 +68,28 @@ $string['config_summary_desc'] = "(Will be displayed in course-search)"; $string['continue'] = 'Continue'; $string['continue_with_empty_course'] = 'Leave the course blank and continue'; +$string['course'] = 'Course'; +$string['course_import'] = 'Course import'; +$string['course_request'] = 'Course request'; +$string['coursedetails_text'] = 'Edit the course details and choose a category the course will be assigned to.'; +$string['coursedetails_title'] = 'Course details'; +$string['courseselect'] = 'Select course'; +$string['courseselect_import'] = 'Choose the course you want to import to Moodle'; +$string['courseselect_request'] = 'Choose the course you want to request.'; +$string['courseselect_validate'] = 'Please choose a course to continue.'; $string['create_mappings'] = 'Create New Mappings'; +$string['created'] = 'Created'; +$string['dashboard_imported_courses'] = 'Imported courses'; +$string['dashboard_request_state_accepted'] = "accepted"; +$string['dashboard_request_state_declined'] = "declined"; +$string['dashboard_request_state_imported'] = "imported"; +$string['dashboard_request_state_pending'] = "pending"; +$string['dashboard_request_teacher'] = "on behalf of "; +$string['dashboard_requested_courses'] = 'Requested courses'; +$string['dashboard_title'] = 'CMS Dashboard'; +$string['dashboard_title_text'] = 'Administrate your courses from external course management systems'; +$string['dashboard_to_course'] = 'Go to course'; +$string['dashboard_wizard_button'] = 'New course request'; $string['db_not_available'] = "The Import-Services is currently not available. Please use the standard request-form. (Link)"; $string['dbhost'] = 'Host'; $string['dbhost_description'] = 'PostgreDB-Host'; @@ -76,6 +104,7 @@ $string['defaultcategory'] = 'Default Category'; $string['defaultcategory_description'] = 'If no category can be matched this category will be offered'; $string['delete'] = 'delete'; +$string['description'] = 'Description'; $string['duplication_timeframe'] = 'Course Duplication Timeframe'; $string['duplication_timeframe_description'] = 'The number of hours after course creation where restore-actions are allowed'; $string['duplication_timeframe_error'] = 'Sorry but for safety reasons it is not allowed import course data from templates or backups more than {$a} hour(s) after course creation'; @@ -128,11 +157,13 @@ $string['max_import_age_description'] = 'The maximum age, a course may have before being imported.'; $string['moodle_url'] = 'Moodle www root'; $string['moodle_url_description'] = 'URL to Moodle www root, which is used to create the link for the his course'; +$string['name'] = 'Name'; $string['navigate'] = 'navigate'; $string['new_request'] = 'request another course'; $string['next_steps'] = "Next Steps"; $string['noConnection'] = "A connection to LSF-Database couldn't be established. Please use the regular formular."; $string['no_template'] = 'Alternative {$a}: Leave the course blank course and continue'; +$string['nocoursesfound'] = "No courses found."; $string['not_mapped'] = 'not mapped'; $string['notice'] = 'Usually Learnweb copies course information from the HIS database and provides these information below. This requires that you are assigned to the course in HIS-LSF as a teacher or assistant. Furthermore your ZIV account has to be assigned to your HIS-LSF profile. If your course is not listed, please choose from the other provided options.'; $string['overwrite'] = 'overwrite'; @@ -165,8 +196,12 @@ $string['roleid_teacher'] = 'RoleID Teacher'; $string['roleid_teacher_description'] = 'role for autoassigned teachers'; $string['select'] = 'Submit'; +$string['semester'] = 'Semester'; +$string['semester_validate'] = 'Please enter a semester.'; $string['shortnamecourse'] = get_string('shortnamecourse') . ' (shortname must contain {$a} at the end) '; $string['shortnameinvalid'] = 'shortname is invalid (it must contain {$a} at the end)'; +$string['shorttitle'] = 'Short title'; +$string['shorttitle_validate'] = 'Please enter a valid short title.'; $string['soappass'] = 'Password'; $string['soappass_description'] = 'Password for the HIS Deeplink service'; $string['soapuser'] = 'soapuser'; @@ -176,12 +211,34 @@ $string['sub_category'] = 'Subcategory'; $string['subcategories'] = 'Unlock Subcategories'; $string['subcategories_description'] = 'Enable choosing not mapped subcategories of mapped categories'; +$string['submit_import_error'] = 'The course could not be imported. Please try again.'; +$string['submit_import_label'] = 'Import course'; +$string['submit_import_success'] = 'The course was imported successfully.'; +$string['submit_request_error'] = 'The course request could not be send. Please try again.'; +$string['submit_request_label'] = 'Request course'; +$string['submit_request_success'] = 'The course request was saved successfully and the teacher was notified. Please wait for an answer.'; +$string['summary_import_text'] = 'Please check the details before submitting the course import.'; +$string['summary_import_title'] = 'Summary of the course import'; +$string['summary_request_text'] = 'Please check the details before submitting the course request.'; +$string['summary_request_title'] = 'Summary of the course request'; $string['task_missing_helptable_entries'] = 'Update table of LSF categories.'; +$string['teacher'] = 'Teacher'; +$string['teachernotfound'] = 'No teacher found'; +$string['teachersearch'] = 'Search for a teacher'; +$string['teachersearch_placeholer'] = 'Enter name or username ...'; +$string['teachersearch_text'] = 'Search for the teacher whose course you want to request'; +$string['teacherselect'] = 'Please select a teacher'; $string['template_from_course'] = 'Alternative {$a}: Include contents from an existing course ...'; +$string['title'] = "Title"; +$string['title_validate'] = "Please enter a valid title."; $string['update_helptable'] = 'Update Helptable With HIS-LSF data'; $string['update_helptable_already_queued'] = 'Task to update tables is already queued. Click here for more information.'; $string['update_helptable_notification'] = 'The Tables will be updated at {$a->time}. Click here for more information.'; +$string['username'] = "Username"; $string['warning_cannot_enrol_nologin'] = "person wasn't enrolled (no username found)"; $string['warning_cannot_enrol_nouser'] = "person wasn't enrolled (no user found)"; $string['warning_cannot_enrol_other'] = "person wasn't enrolled"; $string['warnings'] = 'Warnings:'; +$string['wizard_shell_back'] = 'Back'; +$string['wizard_shell_next'] = 'Next'; +$string['wizard_shell_title'] = 'Course integration'; diff --git a/lib.php b/lib.php index 8de02a4..40d35f1 100644 --- a/lib.php +++ b/lib.php @@ -29,6 +29,11 @@ require_once($CFG->libdir . '/enrollib.php'); require_once($CFG->dirroot . '/user/lib.php'); +// Global lsf_unfication constants. +define('LSF_UNIFICATION_REQUEST_DECLINED', 0); +define('LSF_UNIFICATION_REQUEST_OPEN', 1); +define('LSF_UNIFICATION_REQUEST_ACCEPTED', 2); + /** * get_course_by_idnumber returns the course's id, where idnumber fits $courseid * From 6826733f5111bb0233a4c45ef4e3603feab3e383 Mon Sep 17 00:00:00 2001 From: Tamaro Walter Date: Sun, 23 Aug 2026 20:24:10 +0200 Subject: [PATCH 08/12] backend -request --- classes/local/dto/request_dto.php | 57 +++++++++++++++++++++++++ classes/local/models/course_request.php | 8 +++- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 classes/local/dto/request_dto.php diff --git a/classes/local/dto/request_dto.php b/classes/local/dto/request_dto.php new file mode 100644 index 0000000..dd84b13 --- /dev/null +++ b/classes/local/dto/request_dto.php @@ -0,0 +1,57 @@ +. + +namespace local_lsf_unification\local\dto; + +/** + * DTO for a course request that gets shown in the request manager. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class request_dto { + + /** + * Constructor. + */ + public function __construct( + /** @var int unique id of the request */ + public readonly int $id, + /** @var string title of the course */ + public readonly string $title, + /** @var string user fullname of the person who requested the course */ + public readonly string $requester, + /** @var int request state*/ + public readonly int $requestsstate, + /** @var int created date*/ + public readonly int $created, + ) {} + + /** + * Return the dto in JSON shape. + * @return array + */ + public function to_array(): array { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'requester' => $this->requester, + 'requestsstate' => $this->requestsstate, + 'created' => $this->created, + ]; + } +} \ No newline at end of file diff --git a/classes/local/models/course_request.php b/classes/local/models/course_request.php index adfa3dc..3270cc8 100644 --- a/classes/local/models/course_request.php +++ b/classes/local/models/course_request.php @@ -56,14 +56,17 @@ public function __construct( public readonly int $courseid, /** @var int current state of the request */ private int $state, + /** @var int timestamp of the creation date */ + private int $created, ?int $id = null, ) { global $DB; $this->id = $id ?? $DB->insert_record(self::DB_TABLE_NAME, (object) [ 'requesterid' => $this->requesterid, - 'courseid' => $this->courseid, - 'state' => $this->state, + 'courseid' => $this->courseid, + 'state' => $this->state, + 'created' => $this->created, ]); } @@ -121,6 +124,7 @@ private function get_db_object(): object { 'requesterid' => $this->requesterid, 'courseid' => $this->courseid, 'state' => $this->state, + 'created' => $this->created, ]; } } From 7550477c30f1e61123faab3b25a205d8538edddb Mon Sep 17 00:00:00 2001 From: tamaro Date: Tue, 28 Jul 2026 23:05:21 +0200 Subject: [PATCH 09/12] Add a new frontend for importing/requesting courses A new react frontend is introduced to the plugin. A short summary: - dashboard.php: new entrypoint for the user to access lsf_unification. Only calls the react component - js/Dashboard.tsx: A new dashboard that shows courses that were imported or requested from an cms to moodle - js/services: A service that connects the frontend to the backend rest api - js/wizard: A "Step Wizard" that guides the user through the workflow or either importing an own course or to request a course on behalf of a teacher. --- dashboard.php | 37 +++ js/esm/build/Dashboard.dev.js | 266 ++++++++++++++++++ js/esm/build/Dashboard.dev.js.map | 8 + js/esm/build/Dashboard.js | 6 + js/esm/build/lang.dev.js | 20 ++ js/esm/build/lang.dev.js.map | 8 + js/esm/build/lang.js | 13 + js/esm/build/services/csm.dev.js | 43 +++ js/esm/build/services/csm.dev.js.map | 8 + js/esm/build/services/csm.js | 1 + js/esm/build/wizard/Wizard.dev.js | 114 ++++++++ js/esm/build/wizard/Wizard.dev.js.map | 8 + js/esm/build/wizard/Wizard.js | 8 + .../wizard/components/CourseTable.dev.js | 115 ++++++++ .../wizard/components/CourseTable.dev.js.map | 8 + js/esm/build/wizard/components/CourseTable.js | 7 + .../wizard/components/SummaryPanel.dev.js | 106 +++++++ .../wizard/components/SummaryPanel.dev.js.map | 8 + .../build/wizard/components/SummaryPanel.js | 9 + .../wizard/steps/ChooseActionStep.dev.js | 111 ++++++++ .../wizard/steps/ChooseActionStep.dev.js.map | 8 + js/esm/build/wizard/steps/ChooseActionStep.js | 8 + js/esm/build/wizard/steps/config.dev.js | 44 +++ js/esm/build/wizard/steps/config.dev.js.map | 8 + js/esm/build/wizard/steps/config.js | 8 + .../steps/import/ImportCoursesStep.dev.js | 53 ++++ .../steps/import/ImportCoursesStep.dev.js.map | 8 + .../wizard/steps/import/ImportCoursesStep.js | 7 + .../steps/import/ImportDetailsStep.dev.js | 262 +++++++++++++++++ .../steps/import/ImportDetailsStep.dev.js.map | 8 + .../wizard/steps/import/ImportDetailsStep.js | 8 + .../steps/import/ImportSummaryStep.dev.js | 64 +++++ .../steps/import/ImportSummaryStep.dev.js.map | 8 + .../wizard/steps/import/ImportSummaryStep.js | 8 + .../steps/request/RequestCoursesStep.dev.js | 56 ++++ .../request/RequestCoursesStep.dev.js.map | 8 + .../steps/request/RequestCoursesStep.js | 7 + .../steps/request/RequestSummaryStep.dev.js | 62 ++++ .../request/RequestSummaryStep.dev.js.map | 8 + .../steps/request/RequestSummaryStep.js | 7 + .../steps/request/RequestTeacherStep.dev.js | 168 +++++++++++ .../request/RequestTeacherStep.dev.js.map | 8 + .../steps/request/RequestTeacherStep.js | 9 + js/esm/src/Dashboard.tsx | 141 ++++++++++ js/esm/src/lang.ts | 48 ++++ js/esm/src/services/csm.ts | 75 +++++ js/esm/src/swizzle.json | 6 + js/esm/src/wizard/Wizard.tsx | 98 +++++++ js/esm/src/wizard/components/CourseTable.tsx | 77 +++++ js/esm/src/wizard/components/SummaryPanel.tsx | 88 ++++++ js/esm/src/wizard/steps/ChooseActionStep.tsx | 64 +++++ js/esm/src/wizard/steps/config.tsx | 108 +++++++ .../wizard/steps/import/ImportCoursesStep.tsx | 51 ++++ .../wizard/steps/import/ImportDetailsStep.tsx | 106 +++++++ .../wizard/steps/import/ImportSummaryStep.tsx | 54 ++++ .../steps/request/RequestCoursesStep.tsx | 54 ++++ .../steps/request/RequestSummaryStep.tsx | 52 ++++ .../steps/request/RequestTeacherStep.tsx | 97 +++++++ locallib.php | 33 +++ templates/dashboard/dashboard.mustache | 12 + 60 files changed, 2903 insertions(+) create mode 100644 dashboard.php create mode 100644 js/esm/build/Dashboard.dev.js create mode 100644 js/esm/build/Dashboard.dev.js.map create mode 100644 js/esm/build/Dashboard.js create mode 100644 js/esm/build/lang.dev.js create mode 100644 js/esm/build/lang.dev.js.map create mode 100644 js/esm/build/lang.js create mode 100644 js/esm/build/services/csm.dev.js create mode 100644 js/esm/build/services/csm.dev.js.map create mode 100644 js/esm/build/services/csm.js create mode 100644 js/esm/build/wizard/Wizard.dev.js create mode 100644 js/esm/build/wizard/Wizard.dev.js.map create mode 100644 js/esm/build/wizard/Wizard.js create mode 100644 js/esm/build/wizard/components/CourseTable.dev.js create mode 100644 js/esm/build/wizard/components/CourseTable.dev.js.map create mode 100644 js/esm/build/wizard/components/CourseTable.js create mode 100644 js/esm/build/wizard/components/SummaryPanel.dev.js create mode 100644 js/esm/build/wizard/components/SummaryPanel.dev.js.map create mode 100644 js/esm/build/wizard/components/SummaryPanel.js create mode 100644 js/esm/build/wizard/steps/ChooseActionStep.dev.js create mode 100644 js/esm/build/wizard/steps/ChooseActionStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/ChooseActionStep.js create mode 100644 js/esm/build/wizard/steps/config.dev.js create mode 100644 js/esm/build/wizard/steps/config.dev.js.map create mode 100644 js/esm/build/wizard/steps/config.js create mode 100644 js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js create mode 100644 js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/import/ImportCoursesStep.js create mode 100644 js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js create mode 100644 js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/import/ImportDetailsStep.js create mode 100644 js/esm/build/wizard/steps/import/ImportSummaryStep.dev.js create mode 100644 js/esm/build/wizard/steps/import/ImportSummaryStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/import/ImportSummaryStep.js create mode 100644 js/esm/build/wizard/steps/request/RequestCoursesStep.dev.js create mode 100644 js/esm/build/wizard/steps/request/RequestCoursesStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/request/RequestCoursesStep.js create mode 100644 js/esm/build/wizard/steps/request/RequestSummaryStep.dev.js create mode 100644 js/esm/build/wizard/steps/request/RequestSummaryStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/request/RequestSummaryStep.js create mode 100644 js/esm/build/wizard/steps/request/RequestTeacherStep.dev.js create mode 100644 js/esm/build/wizard/steps/request/RequestTeacherStep.dev.js.map create mode 100644 js/esm/build/wizard/steps/request/RequestTeacherStep.js create mode 100644 js/esm/src/Dashboard.tsx create mode 100644 js/esm/src/lang.ts create mode 100644 js/esm/src/services/csm.ts create mode 100644 js/esm/src/swizzle.json create mode 100644 js/esm/src/wizard/Wizard.tsx create mode 100644 js/esm/src/wizard/components/CourseTable.tsx create mode 100644 js/esm/src/wizard/components/SummaryPanel.tsx create mode 100644 js/esm/src/wizard/steps/ChooseActionStep.tsx create mode 100644 js/esm/src/wizard/steps/config.tsx create mode 100644 js/esm/src/wizard/steps/import/ImportCoursesStep.tsx create mode 100644 js/esm/src/wizard/steps/import/ImportDetailsStep.tsx create mode 100644 js/esm/src/wizard/steps/import/ImportSummaryStep.tsx create mode 100644 js/esm/src/wizard/steps/request/RequestCoursesStep.tsx create mode 100644 js/esm/src/wizard/steps/request/RequestSummaryStep.tsx create mode 100644 js/esm/src/wizard/steps/request/RequestTeacherStep.tsx create mode 100644 locallib.php create mode 100644 templates/dashboard/dashboard.mustache diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..b6023e4 --- /dev/null +++ b/dashboard.php @@ -0,0 +1,37 @@ +. + +/** + * Dashboard of lsf_unification. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +global $CFG, $DB, $PAGE, $USER, $SESSION, $OUTPUT; +require_once($CFG->dirroot . '/local/lsf_unification/locallib.php'); +require_login(); + +lsf_unification_cache_strings(); +$PAGE->set_url(new moodle_url('/local/lsf_unification/dashboard.php')); +$PAGE->set_context(context_system::instance()); +$PAGE->set_title(get_string('pluginname', 'local_lsf_unification')); + +echo $OUTPUT->header(); +echo $OUTPUT->render_from_template('local_lsf_unification/dashboard/dashboard', []); +echo $OUTPUT->footer(); diff --git a/js/esm/build/Dashboard.dev.js b/js/esm/build/Dashboard.dev.js new file mode 100644 index 0000000..91945d1 --- /dev/null +++ b/js/esm/build/Dashboard.dev.js @@ -0,0 +1,266 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { Fragment, jsxDEV } from "react/jsx-dev-runtime"; +/** + * React component that shows the lsf unification dashboard + * @module lsf_unification/Dashboard + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { useEffect, useState } from "react"; +import { fetchDashboardCourses } from "./services/csm"; +import Wizard from "./wizard/Wizard"; +import { str } from "./lang"; +const REQUEST_PENDING = 1; +const REQUEST_ACCEPTED = 2; +const REQUEST_DECLINED = 3; +const REQUEST_IMPORTED = 4; +const REQUEST_STATE_BADGES = { + [REQUEST_PENDING]: { + css: "text-bg-warning", + icon: "fa-regular fa-hourglass-half", + key: "dashboard_request_state_pending" + }, + [REQUEST_ACCEPTED]: { + css: "text-bg-success", + icon: "fa-regular fa-circle-check", + key: "dashboard_request_state_accepted" + }, + [REQUEST_DECLINED]: { + css: "text-bg-danger", + icon: "fa-regular fa-circle-xmark", + key: "dashboard_request_state_declined" + }, + [REQUEST_IMPORTED]: { + css: "text-bg-primary", + icon: "fa-solid fa-file-import", + key: "dashboard_request_state_imported" + } +}; +function Dashboard() { + const [courses, setCourses] = useState([]); + const [wizardOpen, setWizardOpen] = useState(false); + const reloadCourses = /* @__PURE__ */ __name(() => fetchDashboardCourses().then(setCourses), "reloadCourses"); + useEffect(() => { + reloadCourses(); + }, []); + const requested = courses.filter((c) => c.requeststate !== 0); + const created = courses.filter((c) => c.moodleid !== 0); + return /* @__PURE__ */ jsxDEV(Fragment, { children: [ + /* @__PURE__ */ jsxDEV("div", { className: "d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2", children: [ + /* @__PURE__ */ jsxDEV("div", { children: [ + /* @__PURE__ */ jsxDEV("h3", { className: "mb-1", children: str("dashboard_title") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 74, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV("p", { className: "text-muted mb-0", children: str("dashboard_title_text") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 75, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 73, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV("button", { type: "button", className: "btn btn-primary btn-md text-white", onClick: () => setWizardOpen(true), children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-plus me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 78, + columnNumber: 11 + }, this), + str("dashboard_wizard_button") + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 77, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 72, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "card mb-4 shadow-sm", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "card-header bg-white d-flex align-items-center", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-graduation-cap text-primary me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 84, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV("h5", { className: "mb-0", children: str("dashboard_imported_courses") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 85, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV("span", { className: "badge text-bg-secondary rounded-pill ms-2", children: created.length }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 86, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 83, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "list-group list-group-flush", children: created.map((course) => /* @__PURE__ */ jsxDEV("div", { className: "list-group-item d-flex flex-wrap justify-content-between align-items-center py-3", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "me-3", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "fw-semibold", children: course.title }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 92, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "small text-muted", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-regular fa-calendar me-1" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 94, + columnNumber: 19 + }, this), + course.semester + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 93, + columnNumber: 17 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 91, + columnNumber: 15 + }, this), + /* @__PURE__ */ jsxDEV("a", { href: course.moodleurl, className: "btn btn-primary btn-sm text-white", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-arrow-up-right-from-square me-1" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 98, + columnNumber: 17 + }, this), + str("dashboard_to_course") + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 97, + columnNumber: 15 + }, this) + ] }, course.id, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 90, + columnNumber: 13 + }, this)) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 88, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 82, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("hr", { className: "hr" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 104, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "card mb-4 shadow-sm", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "card-header bg-white d-flex align-items-center", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-regular fa-clock text-primary me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 108, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV("h5", { className: "mb-0", children: str("dashboard_requested_courses") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 109, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV("span", { className: "badge text-bg-secondary rounded-pill ms-2", children: requested.length }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 110, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 107, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "list-group list-group-flush", children: requested.map((course) => { + const badge = REQUEST_STATE_BADGES[course.requeststate]; + return /* @__PURE__ */ jsxDEV( + "div", + { + className: "list-group-item border-start-accent d-flex flex-wrap justify-content-between align-items-center py-3", + children: [ + /* @__PURE__ */ jsxDEV("div", { className: "me-3", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "fw-semibold", children: course.title }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 119, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "small text-muted", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-user-tie me-1" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 121, + columnNumber: 19 + }, this), + str("dashboard_request_teacher"), + " ", + course.teacher + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 120, + columnNumber: 17 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 118, + columnNumber: 15 + }, this), + badge && /* @__PURE__ */ jsxDEV("span", { className: `badge ${badge.css} rounded-pill text-white`, children: [ + /* @__PURE__ */ jsxDEV("i", { className: `${badge.icon} me-1` }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 126, + columnNumber: 21 + }, this), + str(badge.key) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 125, + columnNumber: 17 + }, this) + ] + }, + course.id, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 116, + columnNumber: 13 + }, + this + ); + }) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 112, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 106, + columnNumber: 7 + }, this), + wizardOpen && /* @__PURE__ */ jsxDEV(Wizard, { onClose: () => { + setWizardOpen(false); + reloadCourses(); + } }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 135, + columnNumber: 22 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 71, + columnNumber: 5 + }, this); +} +__name(Dashboard, "Dashboard"); +export { + Dashboard as default +}; +//# sourceMappingURL=Dashboard.dev.js.map diff --git a/js/esm/build/Dashboard.dev.js.map b/js/esm/build/Dashboard.dev.js.map new file mode 100644 index 0000000..8a01a94 --- /dev/null +++ b/js/esm/build/Dashboard.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../src/Dashboard.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * React component that shows the lsf unification dashboard\n * @module lsf_unification/Dashboard\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {useEffect, useState} from \"react\";\nimport {Course, fetchDashboardCourses} from \"./services/csm\";\nimport Wizard from \"./wizard/Wizard\";\nimport {str} from \"./lang\";\n\n/** Request states, mirroring the constants of local\\models\\course_request. */\nconst REQUEST_PENDING = 1;\nconst REQUEST_ACCEPTED = 2;\nconst REQUEST_DECLINED = 3;\nconst REQUEST_IMPORTED = 4;\n\n/** How a request state is presented in the badge of a requested course. */\nconst REQUEST_STATE_BADGES: Record = {\n [REQUEST_PENDING]: {\n css: \"text-bg-warning\",\n icon: \"fa-regular fa-hourglass-half\",\n key: \"dashboard_request_state_pending\",\n },\n [REQUEST_ACCEPTED]: {\n css: \"text-bg-success\",\n icon: \"fa-regular fa-circle-check\",\n key: \"dashboard_request_state_accepted\",\n },\n [REQUEST_DECLINED]: {\n css: \"text-bg-danger\",\n icon: \"fa-regular fa-circle-xmark\",\n key: \"dashboard_request_state_declined\",\n },\n [REQUEST_IMPORTED]: {\n css: \"text-bg-primary\",\n icon: \"fa-solid fa-file-import\",\n key: \"dashboard_request_state_imported\",\n },\n};\n\nexport default function Dashboard() {\n const [courses, setCourses] = useState([]);\n const [wizardOpen, setWizardOpen] = useState(false);\n const reloadCourses = () => fetchDashboardCourses().then(setCourses);\n\n useEffect(() => {\n reloadCourses();\n }, []);\n\n const requested = courses.filter((c) => c.requeststate !== 0);\n const created = courses.filter((c) => c.moodleid !== 0);\n\n return (\n <>\n
\n
\n

{str(\"dashboard_title\")}

\n

{str(\"dashboard_title_text\")}

\n
\n \n
\n {/* Current courses. */}\n
\n
\n \n
{str(\"dashboard_imported_courses\")}
\n {created.length}\n
\n
\n {created.map((course) => (\n
\n
\n
{course.title}
\n
\n {course.semester}\n
\n
\n \n {str(\"dashboard_to_course\")}\n \n
\n ))}\n
\n
\n
\n {/* Pending courses*/}\n
\n
\n \n
{str(\"dashboard_requested_courses\")}
\n {requested.length}\n
\n
\n {requested.map((course) => {\n const badge = REQUEST_STATE_BADGES[course.requeststate];\n return (\n
\n
\n
{course.title}
\n
\n {str(\"dashboard_request_teacher\")} {course.teacher}\n
\n
\n {badge && (\n \n {str(badge.key)}\n \n )}\n
\n );\n })}\n
\n
\n {/* Import wizard modal*/}\n {wizardOpen && {\n setWizardOpen(false);\n reloadCourses();\n }}/>}\n \n );\n}"], + "mappings": ";;AAsEI,mBAGM,cAHN;AAvDJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,SAAQ,WAAW,gBAAe;AAClC,SAAgB,6BAA4B;AAC5C,OAAO,YAAY;AACnB,SAAQ,WAAU;AAGlB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AAGzB,MAAM,uBAAiF;AAAA,EACnF,CAAC,eAAe,GAAG;AAAA,IACf,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,EACT;AAAA,EACA,CAAC,gBAAgB,GAAG;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,EACT;AAAA,EACA,CAAC,gBAAgB,GAAG;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,EACT;AAAA,EACA,CAAC,gBAAgB,GAAG;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,EACT;AACJ;AAEe,SAAR,YAA6B;AAChC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAmB,CAAC,CAAC;AACnD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,gBAAgB,6BAAM,sBAAsB,EAAE,KAAK,UAAU,GAA7C;AAEtB,YAAU,MAAM;AACd,kBAAc;AAAA,EAChB,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,iBAAiB,CAAC;AAC5D,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC;AAEtD,SACA,mCACE;AAAA,2BAAC,SAAI,WAAU,0EACb;AAAA,6BAAC,SACC;AAAA,+BAAC,QAAG,WAAU,QAAQ,cAAI,iBAAiB,KAA3C;AAAA;AAAA;AAAA;AAAA,eAA6C;AAAA,QAC7C,uBAAC,OAAE,WAAU,mBAAmB,cAAI,sBAAsB,KAA1D;AAAA;AAAA;AAAA;AAAA,eAA4D;AAAA,WAF9D;AAAA;AAAA;AAAA;AAAA,aAGA;AAAA,MACA,uBAAC,YAAO,MAAK,UAAS,WAAU,qCAAoC,SAAS,MAAM,cAAc,IAAI,GACnG;AAAA,+BAAC,OAAE,WAAU,2BAAb;AAAA;AAAA;AAAA;AAAA,eAAoC;AAAA,QAAG,IAAI,yBAAyB;AAAA,WADtE;AAAA;AAAA;AAAA;AAAA,aAEA;AAAA,SAPF;AAAA;AAAA;AAAA;AAAA,WAQA;AAAA,IAEA,uBAAC,SAAI,WAAU,uBACb;AAAA,6BAAC,SAAI,WAAU,kDACb;AAAA,+BAAC,OAAE,WAAU,kDAAb;AAAA;AAAA;AAAA;AAAA,eAA2D;AAAA,QAC3D,uBAAC,QAAG,WAAU,QAAQ,cAAI,4BAA4B,KAAtD;AAAA;AAAA;AAAA;AAAA,eAAwD;AAAA,QACxD,uBAAC,UAAK,WAAU,6CAA6C,kBAAQ,UAArE;AAAA;AAAA;AAAA;AAAA,eAA4E;AAAA,WAH9E;AAAA;AAAA;AAAA;AAAA,aAIA;AAAA,MACA,uBAAC,SAAI,WAAU,+BACZ,kBAAQ,IAAI,CAAC,WACZ,uBAAC,SAAoB,WAAU,oFAC7B;AAAA,+BAAC,SAAI,WAAU,QACb;AAAA,iCAAC,SAAI,WAAU,eAAe,iBAAO,SAArC;AAAA;AAAA;AAAA;AAAA,iBAA2C;AAAA,UAC3C,uBAAC,SAAI,WAAU,oBACb;AAAA,mCAAC,OAAE,WAAU,iCAAb;AAAA;AAAA;AAAA;AAAA,mBAA0C;AAAA,YAAG,OAAO;AAAA,eADtD;AAAA;AAAA;AAAA;AAAA,iBAEA;AAAA,aAJF;AAAA;AAAA;AAAA;AAAA,eAKA;AAAA,QACA,uBAAC,OAAE,MAAM,OAAO,WAAW,WAAU,qCACnC;AAAA,iCAAC,OAAE,WAAU,iDAAb;AAAA;AAAA;AAAA;AAAA,iBAA0D;AAAA,UAAG,IAAI,qBAAqB;AAAA,aADxF;AAAA;AAAA;AAAA;AAAA,eAEA;AAAA,WATQ,OAAO,IAAjB;AAAA;AAAA;AAAA;AAAA,aAUA,CACD,KAbH;AAAA;AAAA;AAAA;AAAA,aAcA;AAAA,SApBF;AAAA;AAAA;AAAA;AAAA,WAqBA;AAAA,IACA,uBAAC,QAAG,WAAU,QAAd;AAAA;AAAA;AAAA;AAAA,WAAkB;AAAA,IAElB,uBAAC,SAAI,WAAU,uBACb;AAAA,6BAAC,SAAI,WAAU,kDACb;AAAA,+BAAC,OAAE,WAAU,2CAAb;AAAA;AAAA;AAAA;AAAA,eAAoD;AAAA,QACpD,uBAAC,QAAG,WAAU,QAAQ,cAAI,6BAA6B,KAAvD;AAAA;AAAA;AAAA;AAAA,eAAyD;AAAA,QACzD,uBAAC,UAAK,WAAU,6CAA6C,oBAAU,UAAvE;AAAA;AAAA;AAAA;AAAA,eAA8E;AAAA,WAHhF;AAAA;AAAA;AAAA;AAAA,aAIA;AAAA,MACA,uBAAC,SAAI,WAAU,+BACZ,oBAAU,IAAI,CAAC,WAAW;AACzB,cAAM,QAAQ,qBAAqB,OAAO,YAAY;AACtD,eACA;AAAA,UAAC;AAAA;AAAA,YACI,WAAU;AAAA,YACb;AAAA,qCAAC,SAAI,WAAU,QACb;AAAA,uCAAC,SAAI,WAAU,eAAe,iBAAO,SAArC;AAAA;AAAA;AAAA;AAAA,uBAA2C;AAAA,gBAC3C,uBAAC,SAAI,WAAU,oBACb;AAAA,yCAAC,OAAE,WAAU,+BAAb;AAAA;AAAA;AAAA;AAAA,yBAAwC;AAAA,kBAAG,IAAI,2BAA2B;AAAA,kBAAE;AAAA,kBAAE,OAAO;AAAA,qBADvF;AAAA;AAAA;AAAA;AAAA,uBAEA;AAAA,mBAJF;AAAA;AAAA;AAAA;AAAA,qBAKA;AAAA,cACC,SACC,uBAAC,UAAK,WAAW,SAAS,MAAM,GAAG,4BAC/B;AAAA,uCAAC,OAAE,WAAW,GAAG,MAAM,IAAI,WAA3B;AAAA;AAAA;AAAA;AAAA,uBAAmC;AAAA,gBAAG,IAAI,MAAM,GAAG;AAAA,mBADvD;AAAA;AAAA;AAAA;AAAA,qBAEA;AAAA;AAAA;AAAA,UAXM,OAAO;AAAA,UAAjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAaA;AAAA,MAEF,CAAC,KAnBH;AAAA;AAAA;AAAA;AAAA,aAoBA;AAAA,SA1BF;AAAA;AAAA;AAAA;AAAA,WA2BA;AAAA,IAEC,cAAc,uBAAC,UAAO,SAAS,MAAM;AACpC,oBAAc,KAAK;AACnB,oBAAc;AAAA,IAChB,KAHe;AAAA;AAAA;AAAA;AAAA,WAGb;AAAA,OAnEJ;AAAA;AAAA;AAAA;AAAA,SAoEA;AAEJ;AAnFwB;", + "names": [] +} diff --git a/js/esm/build/Dashboard.js b/js/esm/build/Dashboard.js new file mode 100644 index 0000000..b811c7d --- /dev/null +++ b/js/esm/build/Dashboard.js @@ -0,0 +1,6 @@ +import{useEffect as f,useState as o}from"react";import{fetchDashboardCourses as b}from"./services/csm";import p from"./wizard/Wizard";import{str as t}from"./lang";import{Fragment as y,jsx as e,jsxs as a}from"react/jsx-runtime";/** + * React component that shows the lsf unification dashboard + * @module lsf_unification/Dashboard + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */const h=1,g=2,N=3,v=4,_={[h]:{css:"text-bg-warning",icon:"fa-regular fa-hourglass-half",key:"dashboard_request_state_pending"},[g]:{css:"text-bg-success",icon:"fa-regular fa-circle-check",key:"dashboard_request_state_accepted"},[N]:{css:"text-bg-danger",icon:"fa-regular fa-circle-xmark",key:"dashboard_request_state_declined"},[v]:{css:"text-bg-primary",icon:"fa-solid fa-file-import",key:"dashboard_request_state_imported"}};function x(){const[i,n]=o([]),[u,d]=o(!1),l=()=>b().then(n);f(()=>{l()},[]);const c=i.filter(s=>s.requeststate!==0),m=i.filter(s=>s.moodleid!==0);return a(y,{children:[a("div",{className:"d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2",children:[a("div",{children:[e("h3",{className:"mb-1",children:t("dashboard_title")}),e("p",{className:"text-muted mb-0",children:t("dashboard_title_text")})]}),a("button",{type:"button",className:"btn btn-primary btn-md text-white",onClick:()=>d(!0),children:[e("i",{className:"fa-solid fa-plus me-2"}),t("dashboard_wizard_button")]})]}),a("div",{className:"card mb-4 shadow-sm",children:[a("div",{className:"card-header bg-white d-flex align-items-center",children:[e("i",{className:"fa-solid fa-graduation-cap text-primary me-2"}),e("h5",{className:"mb-0",children:t("dashboard_imported_courses")}),e("span",{className:"badge text-bg-secondary rounded-pill ms-2",children:m.length})]}),e("div",{className:"list-group list-group-flush",children:m.map(s=>a("div",{className:"list-group-item d-flex flex-wrap justify-content-between align-items-center py-3",children:[a("div",{className:"me-3",children:[e("div",{className:"fw-semibold",children:s.title}),a("div",{className:"small text-muted",children:[e("i",{className:"fa-regular fa-calendar me-1"}),s.semester]})]}),a("a",{href:s.moodleurl,className:"btn btn-primary btn-sm text-white",children:[e("i",{className:"fa-solid fa-arrow-up-right-from-square me-1"}),t("dashboard_to_course")]})]},s.id))})]}),e("hr",{className:"hr"}),a("div",{className:"card mb-4 shadow-sm",children:[a("div",{className:"card-header bg-white d-flex align-items-center",children:[e("i",{className:"fa-regular fa-clock text-primary me-2"}),e("h5",{className:"mb-0",children:t("dashboard_requested_courses")}),e("span",{className:"badge text-bg-secondary rounded-pill ms-2",children:c.length})]}),e("div",{className:"list-group list-group-flush",children:c.map(s=>{const r=_[s.requeststate];return a("div",{className:"list-group-item border-start-accent d-flex flex-wrap justify-content-between align-items-center py-3",children:[a("div",{className:"me-3",children:[e("div",{className:"fw-semibold",children:s.title}),a("div",{className:"small text-muted",children:[e("i",{className:"fa-solid fa-user-tie me-1"}),t("dashboard_request_teacher")," ",s.teacher]})]}),r&&a("span",{className:`badge ${r.css} rounded-pill text-white`,children:[e("i",{className:`${r.icon} me-1`}),t(r.key)]})]},s.id)})})]}),u&&e(p,{onClose:()=>{d(!1),l()}})]})}export{x as default}; diff --git a/js/esm/build/lang.dev.js b/js/esm/build/lang.dev.js new file mode 100644 index 0000000..cb7fda7 --- /dev/null +++ b/js/esm/build/lang.dev.js @@ -0,0 +1,20 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +/** + * Language strings of this plugin, the way get_string() works in PHP. + * + * This is synchronous, unlike core's own string API: dashboard.php calls + * lsf_unification_cache_strings(), which hands every string of this component to the page + * with strings_for_js(), so they are all in memory by the time anything renders. Any other + * page that mounts these components has to do the same, otherwise every string shows up as + * "[[key,local_lsf_unification]]". + * + * @module lsf_unification/lang + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const str = /* @__PURE__ */ __name((key, param) => M.util.get_string(key, "local_lsf_unification", param), "str"); +export { + str +}; +//# sourceMappingURL=lang.dev.js.map diff --git a/js/esm/build/lang.dev.js.map b/js/esm/build/lang.dev.js.map new file mode 100644 index 0000000..d01692f --- /dev/null +++ b/js/esm/build/lang.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../src/lang.ts"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Language strings of this plugin, the way get_string() works in PHP.\n *\n * This is synchronous, unlike core's own string API: dashboard.php calls\n * lsf_unification_cache_strings(), which hands every string of this component to the page\n * with strings_for_js(), so they are all in memory by the time anything renders. Any other\n * page that mounts these components has to do the same, otherwise every string shows up as\n * \"[[key,local_lsf_unification]]\".\n *\n * @module lsf_unification/lang\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/** Moodle's global string helper. */\ndeclare const M: {\n util: {\n get_string: (identifier: string, component: string, param?: StringParams) => string;\n };\n};\n\n/** What can be substituted into a string's placeholders, the equivalent of PHP's $a. */\ntype StringParams = Record | string | number;\n\n/**\n * A language string of this plugin.\n *\n * @param key The string identifier, as in the lang file.\n * @param param Fills the string's placeholders, like the $a argument in PHP.\n * @returns The translated string, or \"[[key,local_lsf_unification]]\" if it is not defined.\n */\nexport const str = (key: string, param?: StringParams): string =>\n M.util.get_string(key, \"local_lsf_unification\", param);\n"], + "mappings": ";;AAeA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+BO,MAAM,MAAM,wBAAC,KAAa,UAC7B,EAAE,KAAK,WAAW,KAAK,yBAAyB,KAAK,GADtC;", + "names": [] +} diff --git a/js/esm/build/lang.js b/js/esm/build/lang.js new file mode 100644 index 0000000..904ec18 --- /dev/null +++ b/js/esm/build/lang.js @@ -0,0 +1,13 @@ +/** + * Language strings of this plugin, the way get_string() works in PHP. + * + * This is synchronous, unlike core's own string API: dashboard.php calls + * lsf_unification_cache_strings(), which hands every string of this component to the page + * with strings_for_js(), so they are all in memory by the time anything renders. Any other + * page that mounts these components has to do the same, otherwise every string shows up as + * "[[key,local_lsf_unification]]". + * + * @module lsf_unification/lang + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */const n=(t,r)=>M.util.get_string(t,"local_lsf_unification",r);export{n as str}; diff --git a/js/esm/build/services/csm.dev.js b/js/esm/build/services/csm.dev.js new file mode 100644 index 0000000..0923388 --- /dev/null +++ b/js/esm/build/services/csm.dev.js @@ -0,0 +1,43 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import Fetch from "@moodle/lms/core/fetch"; +const fetchDashboardCourses = /* @__PURE__ */ __name(async () => { + const response = await Fetch.performGet("local_lsf_unification", "dashboard/dashboardcourses"); + return await response.json(); +}, "fetchDashboardCourses"); +const fetchOwnCourses = /* @__PURE__ */ __name(async () => { + const response = await Fetch.performGet("local_lsf_unification", "dashboard/courses"); + return await response.json(); +}, "fetchOwnCourses"); +const fetchTeacherCourses = /* @__PURE__ */ __name(async (username) => { + const response = await Fetch.performGet("local_lsf_unification", `dashboard/courses/${username}`); + return await response.json(); +}, "fetchTeacherCourses"); +const submitCourseRequest = /* @__PURE__ */ __name(async (cache) => { + const response = await Fetch.performPost("local_lsf_unification", "dashboard/request", { body: cache }); + const payload = await response.json(); + return payload.status; +}, "submitCourseRequest"); +const submitCourseImport = /* @__PURE__ */ __name(async (cache) => { + const response = await Fetch.performPost("local_lsf_unification", "dashboard/import", { body: cache }); + const payload = await response.json(); + return payload.status; +}, "submitCourseImport"); +const fetchTeachers = /* @__PURE__ */ __name(async () => { + const response = await Fetch.performGet("local_lsf_unification", "dashboard/teachers"); + return await response.json(); +}, "fetchTeachers"); +const fetchCategories = /* @__PURE__ */ __name(async () => { + const response = await Fetch.performGet("local_lsf_unification", "dashboard/categories"); + return await response.json(); +}, "fetchCategories"); +export { + fetchCategories, + fetchDashboardCourses, + fetchOwnCourses, + fetchTeacherCourses, + fetchTeachers, + submitCourseImport, + submitCourseRequest +}; +//# sourceMappingURL=csm.dev.js.map diff --git a/js/esm/build/services/csm.dev.js.map b/js/esm/build/services/csm.dev.js.map new file mode 100644 index 0000000..ee61d24 --- /dev/null +++ b/js/esm/build/services/csm.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../src/services/csm.ts"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["import Fetch from \"@moodle/lms/core/fetch\";\nimport type {WizardCache} from \"../wizard/steps/config\";\n\nexport type Course = {\n id: number;\n cms: string;\n cmsinstance: number;\n cmsurl: string;\n title: string;\n shorttitle: string;\n description: string;\n teacher: string;\n semester: string;\n created: number;\n courseid: number;\n moodleid: number;\n requeststate: number;\n moodleurl: string;\n};\n\nexport type Teacher = {\n username: string;\n firstname: string;\n lastname: string;\n};\n\nexport type Category = {\n id: number;\n name: string;\n};\n\nexport const fetchDashboardCourses = async(): Promise => {\n const response = await Fetch.performGet(\"local_lsf_unification\", \"dashboard/dashboardcourses\");\n return await response.json() as Promise;\n};\n\nexport const fetchOwnCourses = async(): Promise => {\n const response = await Fetch.performGet(\"local_lsf_unification\", \"dashboard/courses\");\n return await response.json() as Promise;\n};\n\nexport const fetchTeacherCourses = async(username: string): Promise => {\n const response = await Fetch.performGet(\"local_lsf_unification\", `dashboard/courses/${username}`);\n return await response.json() as Promise;\n};\n\n/**\n * Submits a course request built from the whole wizard cache (branch, teacher,\n * course, ...). Resolves with the status the controller reports back.\n */\nexport const submitCourseRequest = async(cache: WizardCache): Promise => {\n const response = await Fetch.performPost(\"local_lsf_unification\", \"dashboard/request\", {body: cache});\n const payload = await response.json() as {status: boolean};\n return payload.status;\n};\n\n/**\n * Submits a course import built from the whole wizard cache (course details, category).\n */\nexport const submitCourseImport = async(cache: WizardCache): Promise => {\n const response = await Fetch.performPost(\"local_lsf_unification\", \"dashboard/import\", {body: cache});\n const payload = await response.json() as {status: boolean};\n return payload.status;\n};\n\nexport const fetchTeachers = async(): Promise => {\n const response = await Fetch.performGet(\"local_lsf_unification\", \"dashboard/teachers\");\n return await response.json() as Promise;\n};\n\n/** Loads all Moodle course categories the user may pick from for the import. */\nexport const fetchCategories = async(): Promise => {\n const response = await Fetch.performGet(\"local_lsf_unification\", \"dashboard/categories\");\n return await response.json() as Promise;\n};\n"], + "mappings": ";;AAAA,OAAO,WAAW;AA+BX,MAAM,wBAAwB,mCAA8B;AAC/D,QAAM,WAAW,MAAM,MAAM,WAAW,yBAAyB,4BAA4B;AAC7F,SAAO,MAAM,SAAS,KAAK;AAC/B,GAHqC;AAK9B,MAAM,kBAAkB,mCAA8B;AACzD,QAAM,WAAW,MAAM,MAAM,WAAW,yBAAyB,mBAAmB;AACpF,SAAO,MAAM,SAAS,KAAK;AAC/B,GAH+B;AAKxB,MAAM,sBAAsB,8BAAM,aAAwC;AAC7E,QAAM,WAAW,MAAM,MAAM,WAAW,yBAAyB,qBAAqB,QAAQ,EAAE;AAChG,SAAO,MAAM,SAAS,KAAK;AAC/B,GAHmC;AAS5B,MAAM,sBAAsB,8BAAM,UAAyC;AAC9E,QAAM,WAAW,MAAM,MAAM,YAAY,yBAAyB,qBAAqB,EAAC,MAAM,MAAK,CAAC;AACpG,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,SAAO,QAAQ;AACnB,GAJmC;AAS5B,MAAM,qBAAqB,8BAAM,UAAyC;AAC7E,QAAM,WAAW,MAAM,MAAM,YAAY,yBAAyB,oBAAoB,EAAC,MAAM,MAAK,CAAC;AACnG,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,SAAO,QAAQ;AACnB,GAJkC;AAM3B,MAAM,gBAAgB,mCAA+B;AACxD,QAAM,WAAW,MAAM,MAAM,WAAW,yBAAyB,oBAAoB;AACrF,SAAO,MAAM,SAAS,KAAK;AAC/B,GAH6B;AAMtB,MAAM,kBAAkB,mCAAgC;AAC3D,QAAM,WAAW,MAAM,MAAM,WAAW,yBAAyB,sBAAsB;AACvF,SAAO,MAAM,SAAS,KAAK;AAC/B,GAH+B;", + "names": [] +} diff --git a/js/esm/build/services/csm.js b/js/esm/build/services/csm.js new file mode 100644 index 0000000..fd875a4 --- /dev/null +++ b/js/esm/build/services/csm.js @@ -0,0 +1 @@ +import e from"@moodle/lms/core/fetch";const a=async()=>await(await e.performGet("local_lsf_unification","dashboard/dashboardcourses")).json(),n=async()=>await(await e.performGet("local_lsf_unification","dashboard/courses")).json(),i=async s=>await(await e.performGet("local_lsf_unification",`dashboard/courses/${s}`)).json(),c=async s=>(await(await e.performPost("local_lsf_unification","dashboard/request",{body:s})).json()).status,u=async s=>(await(await e.performPost("local_lsf_unification","dashboard/import",{body:s})).json()).status,p=async()=>await(await e.performGet("local_lsf_unification","dashboard/teachers")).json(),m=async()=>await(await e.performGet("local_lsf_unification","dashboard/categories")).json();export{m as fetchCategories,a as fetchDashboardCourses,n as fetchOwnCourses,i as fetchTeacherCourses,p as fetchTeachers,u as submitCourseImport,c as submitCourseRequest}; diff --git a/js/esm/build/wizard/Wizard.dev.js b/js/esm/build/wizard/Wizard.dev.js new file mode 100644 index 0000000..96cbadb --- /dev/null +++ b/js/esm/build/wizard/Wizard.dev.js @@ -0,0 +1,114 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { Fragment, jsxDEV } from "react/jsx-dev-runtime"; +/** + * The wizard shell: it owns the cache the steps fill up, which step is showing, and the Back/Next navigation. + * It knows nothing about the individual steps beyond what the stepvmap in steps/config tells it. + * + * @module lsf_unification/wizard/Wizard + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { useState, useEffect } from "react"; +import { STEPS, sequenceFor, canAdvance } from "./steps/config"; +import { str } from "../lang"; +function Wizard({ onClose }) { + const [step, setStep] = useState(0); + const [showErrors, setShowErrors] = useState(false); + const [cache, setCache] = useState({ branch: null, course: null, teacher: null, category: null, submitted: false }); + const patchCache = /* @__PURE__ */ __name((partial) => setCache((c) => ({ ...c, ...partial })), "patchCache"); + useEffect(() => { + const onKey = /* @__PURE__ */ __name((e) => e.key === "Escape" && onClose(), "onKey"); + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [onClose]); + const sequence = sequenceFor(cache.branch); + const currentStepId = sequence[step]; + const { component: Step } = STEPS[currentStepId]; + const isFirst = step === 0; + const isLast = step === sequence.length - 1; + const title = currentStepId === "choose" || cache.branch === null ? str("wizard_shell_title") : str(cache.branch === "import" ? "course_import" : "course_request"); + const back = /* @__PURE__ */ __name(() => { + setShowErrors(false); + setStep((s) => s - 1); + }, "back"); + const next = /* @__PURE__ */ __name(() => { + if (!canAdvance(currentStepId, cache)) { + setShowErrors(true); + return; + } + setShowErrors(false); + setStep((s) => s + 1); + }, "next"); + return /* @__PURE__ */ jsxDEV(Fragment, { children: [ + /* @__PURE__ */ jsxDEV("div", { className: "modal fade show d-block", tabIndex: -1, role: "dialog", "aria-modal": "true", children: /* @__PURE__ */ jsxDEV("div", { className: "modal-dialog modal-lg modal-dialog-centered", children: /* @__PURE__ */ jsxDEV("div", { className: "modal-content shadow", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "modal-header", children: [ + /* @__PURE__ */ jsxDEV("h5", { className: "modal-title", children: title }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 74, + columnNumber: 15 + }, this), + /* @__PURE__ */ jsxDEV("button", { type: "button", className: "btn-close", "aria-label": str("close"), onClick: onClose }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 75, + columnNumber: 15 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 73, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "modal-body", children: /* @__PURE__ */ jsxDEV(Step, { cache, patch: patchCache, showErrors }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 78, + columnNumber: 15 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 77, + columnNumber: 13 + }, this), + !cache.submitted && /* @__PURE__ */ jsxDEV("div", { className: "modal-footer justify-content-between", children: [ + /* @__PURE__ */ jsxDEV("button", { type: "button", className: "btn btn-outline-secondary", onClick: back, disabled: isFirst, children: str("wizard_shell_back") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 82, + columnNumber: 17 + }, this), + !isLast && /* @__PURE__ */ jsxDEV("button", { type: "button", className: "btn btn-primary text-white", onClick: next, children: str("wizard_shell_next") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 86, + columnNumber: 19 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 81, + columnNumber: 15 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 72, + columnNumber: 11 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 71, + columnNumber: 9 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 70, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "modal-backdrop fade show" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 95, + columnNumber: 7 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/Wizard.tsx", + lineNumber: 69, + columnNumber: 5 + }, this); +} +__name(Wizard, "Wizard"); +export { + Wizard as default +}; +//# sourceMappingURL=Wizard.dev.js.map diff --git a/js/esm/build/wizard/Wizard.dev.js.map b/js/esm/build/wizard/Wizard.dev.js.map new file mode 100644 index 0000000..2c34949 --- /dev/null +++ b/js/esm/build/wizard/Wizard.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../src/wizard/Wizard.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The wizard shell: it owns the cache the steps fill up, which step is showing, and the Back/Next navigation.\n * It knows nothing about the individual steps beyond what the stepvmap in steps/config tells it.\n *\n * @module lsf_unification/wizard/Wizard\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {useState, useEffect} from \"react\";\nimport {STEPS, sequenceFor, canAdvance} from \"./steps/config\";\nimport type {WizardCache} from \"./steps/config\";\nimport {str} from \"../lang\";\n\nexport default function Wizard({onClose}: {onClose: () => void}) {\n const [step, setStep] = useState(0);\n const [showErrors, setShowErrors] = useState(false);\n const [cache, setCache] = useState({branch: null, course: null, teacher: null, category: null, submitted: false});\n\n const patchCache = (partial: Partial) => setCache((c) => ({...c, ...partial}));\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && onClose();\n document.addEventListener(\"keydown\", onKey);\n\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [onClose]);\n\n const sequence = sequenceFor(cache.branch);\n const currentStepId = sequence[step];\n const {component: Step} = STEPS[currentStepId];\n const isFirst = step === 0;\n const isLast = step === sequence.length - 1;\n\n const title = currentStepId === \"choose\" || cache.branch === null\n ? str(\"wizard_shell_title\")\n : str(cache.branch === \"import\" ? \"course_import\" : \"course_request\");\n\n const back = () => {\n setShowErrors(false);\n setStep((s) => s - 1);\n };\n\n const next = () => {\n if (!canAdvance(currentStepId, cache)) {\n setShowErrors(true);\n return;\n }\n setShowErrors(false);\n setStep((s) => s + 1);\n };\n\n return (\n <>\n
\n
\n
\n
\n
{title}
\n
\n
\n \n
\n {!cache.submitted && (\n
\n \n {!isLast && (\n \n )}\n
\n )}\n
\n
\n
\n
\n \n );\n}\n"], + "mappings": ";;AAoEI,mBAKU,cALV;AArDJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,SAAQ,UAAU,iBAAgB;AAClC,SAAQ,OAAO,aAAa,kBAAiB;AAE7C,SAAQ,WAAU;AAEH,SAAR,OAAwB,EAAC,QAAO,GAA0B;AAC/D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsB,EAAC,QAAQ,MAAM,QAAQ,MAAM,SAAS,MAAM,UAAU,MAAM,WAAW,MAAK,CAAC;AAE7H,QAAM,aAAa,wBAAC,YAAkC,SAAS,CAAC,OAAO,EAAC,GAAG,GAAG,GAAG,QAAO,EAAE,GAAvE;AAEnB,YAAU,MAAM;AACd,UAAM,QAAQ,wBAAC,MAAqB,EAAE,QAAQ,YAAY,QAAQ,GAApD;AACd,aAAS,iBAAiB,WAAW,KAAK;AAE1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,WAAW,YAAY,MAAM,MAAM;AACzC,QAAM,gBAAgB,SAAS,IAAI;AACnC,QAAM,EAAC,WAAW,KAAI,IAAI,MAAM,aAAa;AAC7C,QAAM,UAAU,SAAS;AACzB,QAAM,SAAS,SAAS,SAAS,SAAS;AAE1C,QAAM,QAAQ,kBAAkB,YAAY,MAAM,WAAW,OACzD,IAAI,oBAAoB,IACxB,IAAI,MAAM,WAAW,WAAW,kBAAkB,gBAAgB;AAEtE,QAAM,OAAO,6BAAM;AACjB,kBAAc,KAAK;AACnB,YAAQ,CAAC,MAAM,IAAI,CAAC;AAAA,EACtB,GAHa;AAKb,QAAM,OAAO,6BAAM;AACjB,QAAI,CAAC,WAAW,eAAe,KAAK,GAAG;AACrC,oBAAc,IAAI;AAClB;AAAA,IACF;AACA,kBAAc,KAAK;AACnB,YAAQ,CAAC,MAAM,IAAI,CAAC;AAAA,EACtB,GAPa;AASb,SACE,mCACE;AAAA,2BAAC,SAAI,WAAU,2BAA0B,UAAU,IAAI,MAAK,UAAS,cAAW,QAC9E,iCAAC,SAAI,WAAU,+CACb,iCAAC,SAAI,WAAU,wBACb;AAAA,6BAAC,SAAI,WAAU,gBACb;AAAA,+BAAC,QAAG,WAAU,eAAe,mBAA7B;AAAA;AAAA;AAAA;AAAA,eAAmC;AAAA,QACnC,uBAAC,YAAO,MAAK,UAAS,WAAU,aAAY,cAAY,IAAI,OAAO,GAAG,SAAS,WAA/E;AAAA;AAAA;AAAA;AAAA,eAAuF;AAAA,WAFzF;AAAA;AAAA;AAAA;AAAA,aAGA;AAAA,MACA,uBAAC,SAAI,WAAU,cACb,iCAAC,QAAK,OAAc,OAAO,YAAY,cAAvC;AAAA;AAAA;AAAA;AAAA,aAA8D,KADhE;AAAA;AAAA;AAAA;AAAA,aAEA;AAAA,MACC,CAAC,MAAM,aACN,uBAAC,SAAI,WAAU,wCACb;AAAA,+BAAC,YAAO,MAAK,UAAS,WAAU,6BAA4B,SAAS,MAAM,UAAU,SAClF,cAAI,mBAAmB,KAD1B;AAAA;AAAA;AAAA;AAAA,eAEA;AAAA,QACC,CAAC,UACA,uBAAC,YAAO,MAAK,UAAS,WAAU,8BAA6B,SAAS,MACnE,cAAI,mBAAmB,KAD1B;AAAA;AAAA;AAAA;AAAA,eAEA;AAAA,WAPJ;AAAA;AAAA;AAAA;AAAA,aASA;AAAA,SAlBJ;AAAA;AAAA;AAAA;AAAA,WAoBA,KArBF;AAAA;AAAA;AAAA;AAAA,WAsBA,KAvBF;AAAA;AAAA;AAAA;AAAA,WAwBA;AAAA,IACA,uBAAC,SAAI,WAAU,8BAAf;AAAA;AAAA;AAAA;AAAA,WAAyC;AAAA,OA1B3C;AAAA;AAAA;AAAA;AAAA,SA2BA;AAEJ;AApEwB;", + "names": [] +} diff --git a/js/esm/build/wizard/Wizard.js b/js/esm/build/wizard/Wizard.js new file mode 100644 index 0000000..6fe31ed --- /dev/null +++ b/js/esm/build/wizard/Wizard.js @@ -0,0 +1,8 @@ +import{useState as i,useEffect as E}from"react";import{STEPS as _,sequenceFor as g,canAdvance as z}from"./steps/config";import{str as o}from"../lang";import{Fragment as S,jsx as e,jsxs as s}from"react/jsx-runtime";/** + * The wizard shell: it owns the cache the steps fill up, which step is showing, and the Back/Next navigation. + * It knows nothing about the individual steps beyond what the stepvmap in steps/config tells it. + * + * @module lsf_unification/wizard/Wizard + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */function C({onClose:n}){const[c,m]=i(0),[b,r]=i(!1),[a,h]=i({branch:null,course:null,teacher:null,category:null,submitted:!1}),p=t=>h(d=>({...d,...t}));E(()=>{const t=d=>d.key==="Escape"&&n();return document.addEventListener("keydown",t),()=>document.removeEventListener("keydown",t)},[n]);const u=g(a.branch),l=u[c],{component:v}=_[l],f=c===0,y=c===u.length-1,w=l==="choose"||a.branch===null?o("wizard_shell_title"):o(a.branch==="import"?"course_import":"course_request"),N=()=>{r(!1),m(t=>t-1)},k=()=>{if(!z(l,a)){r(!0);return}r(!1),m(t=>t+1)};return s(S,{children:[e("div",{className:"modal fade show d-block",tabIndex:-1,role:"dialog","aria-modal":"true",children:e("div",{className:"modal-dialog modal-lg modal-dialog-centered",children:s("div",{className:"modal-content shadow",children:[s("div",{className:"modal-header",children:[e("h5",{className:"modal-title",children:w}),e("button",{type:"button",className:"btn-close","aria-label":o("close"),onClick:n})]}),e("div",{className:"modal-body",children:e(v,{cache:a,patch:p,showErrors:b})}),!a.submitted&&s("div",{className:"modal-footer justify-content-between",children:[e("button",{type:"button",className:"btn btn-outline-secondary",onClick:N,disabled:f,children:o("wizard_shell_back")}),!y&&e("button",{type:"button",className:"btn btn-primary text-white",onClick:k,children:o("wizard_shell_next")})]})]})})}),e("div",{className:"modal-backdrop fade show"})]})}export{C as default}; diff --git a/js/esm/build/wizard/components/CourseTable.dev.js b/js/esm/build/wizard/components/CourseTable.dev.js new file mode 100644 index 0000000..cdc68e3 --- /dev/null +++ b/js/esm/build/wizard/components/CourseTable.dev.js @@ -0,0 +1,115 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { jsxDEV } from "react/jsx-dev-runtime"; +/** + * A reusable html component that lists cms courses to pick one from. Both import and request use it to show courses. + * + * @module lsf_unification/wizard/components/CourseTable + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { str } from "../../lang"; +const formatCreated = /* @__PURE__ */ __name((created) => new Date(created * 1e3).toLocaleDateString( + "de-DE", + { timeZone: "Europe/Berlin", day: "2-digit", month: "2-digit", year: "numeric" } +), "formatCreated"); +function CourseTable({ courses, selected, onSelect }) { + if (courses.length === 0) { + return /* @__PURE__ */ jsxDEV("p", { className: "text-center text-muted py-4 mb-0", children: /* @__PURE__ */ jsxDEV("em", { children: str("nocoursesfound") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 45, + columnNumber: 60 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 45, + columnNumber: 12 + }, this); + } + return /* @__PURE__ */ jsxDEV("div", { className: "table-responsive", children: /* @__PURE__ */ jsxDEV("table", { className: "table table-hover table-borderless align-middle mb-0", children: [ + /* @__PURE__ */ jsxDEV("thead", { children: /* @__PURE__ */ jsxDEV("tr", { children: [ + /* @__PURE__ */ jsxDEV("th", { scope: "col", children: str("title") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 53, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("th", { scope: "col", children: str("semester") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 54, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("th", { scope: "col", children: str("created") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 55, + columnNumber: 13 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 52, + columnNumber: 11 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 51, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV("tbody", { children: courses.map((course) => { + const isSelected = selected?.id === course.id; + return /* @__PURE__ */ jsxDEV( + "tr", + { + className: isSelected ? "table-primary" : "", + style: { cursor: "pointer" }, + onClick: () => onSelect(course), + children: [ + /* @__PURE__ */ jsxDEV("td", { className: "fw-semibold", children: [ + isSelected && /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-check text-primary me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 65, + columnNumber: 34 + }, this), + course.title + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 64, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV("td", { children: course.semester }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 68, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV("td", { children: formatCreated(course.created) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 69, + columnNumber: 17 + }, this) + ] + }, + course.id, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 62, + columnNumber: 15 + }, + this + ); + }) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 58, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 50, + columnNumber: 7 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/CourseTable.tsx", + lineNumber: 49, + columnNumber: 5 + }, this); +} +__name(CourseTable, "CourseTable"); +export { + CourseTable as default +}; +//# sourceMappingURL=CourseTable.dev.js.map diff --git a/js/esm/build/wizard/components/CourseTable.dev.js.map b/js/esm/build/wizard/components/CourseTable.dev.js.map new file mode 100644 index 0000000..e3b7700 --- /dev/null +++ b/js/esm/build/wizard/components/CourseTable.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../src/wizard/components/CourseTable.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A reusable html component that lists cms courses to pick one from. Both import and request use it to show courses.\n *\n * @module lsf_unification/wizard/components/CourseTable\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport type {Course} from \"../../services/csm\";\nimport {str} from \"../../lang\";\n\n/**\n * The data that gets shown: the courses to list, the selected course and a function that updates the course in the cache.\n */\ntype Props = {\n courses: Course[];\n selected: Course | null;\n onSelect: (course: Course) => void;\n};\n\n/**\n * Formats a unix timestamp into a readable date.\n */\nconst formatCreated = (created: number): string =>\n new Date(created * 1000).toLocaleDateString(\"de-DE\",\n {timeZone: \"Europe/Berlin\", day: \"2-digit\", month: \"2-digit\", year: \"numeric\"});\n\nexport default function CourseTable({courses, selected, onSelect}: Props) {\n if (courses.length === 0) {\n return

{str(\"nocoursesfound\")}

;\n }\n\n return (\n
\n \n \n \n \n \n \n \n \n \n {courses.map((course) => {\n const isSelected = selected?.id === course.id;\n return (\n onSelect(course)}>\n \n \n \n \n );\n })}\n \n
{str(\"title\")}{str(\"semester\")}{str(\"created\")}
\n {isSelected && }\n {course.title}\n {course.semester}{formatCreated(course.created)}
\n
\n );\n}\n"], + "mappings": ";;AA4C2D;AA7B3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,SAAQ,WAAU;AAclB,MAAM,gBAAgB,wBAAC,YACrB,IAAI,KAAK,UAAU,GAAI,EAAE;AAAA,EAAmB;AAAA,EAC1C,EAAC,UAAU,iBAAiB,KAAK,WAAW,OAAO,WAAW,MAAM,UAAS;AAAC,GAF5D;AAIP,SAAR,YAA6B,EAAC,SAAS,UAAU,SAAQ,GAAU;AACxE,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,uBAAC,OAAE,WAAU,oCAAmC,iCAAC,QAAI,cAAI,gBAAgB,KAAzB;AAAA;AAAA;AAAA;AAAA,WAA2B,KAA3E;AAAA;AAAA;AAAA;AAAA,WAAgF;AAAA,EACzF;AAEA,SACE,uBAAC,SAAI,WAAU,oBACb,iCAAC,WAAM,WAAU,wDACf;AAAA,2BAAC,WACC,iCAAC,QACC;AAAA,6BAAC,QAAG,OAAM,OAAO,cAAI,OAAO,KAA5B;AAAA;AAAA;AAAA;AAAA,aAA8B;AAAA,MAC9B,uBAAC,QAAG,OAAM,OAAO,cAAI,UAAU,KAA/B;AAAA;AAAA;AAAA;AAAA,aAAiC;AAAA,MACjC,uBAAC,QAAG,OAAM,OAAO,cAAI,SAAS,KAA9B;AAAA;AAAA;AAAA;AAAA,aAAgC;AAAA,SAHlC;AAAA;AAAA;AAAA;AAAA,WAIA,KALF;AAAA;AAAA;AAAA;AAAA,WAMA;AAAA,IACA,uBAAC,WACE,kBAAQ,IAAI,CAAC,WAAW;AACvB,YAAM,aAAa,UAAU,OAAO,OAAO;AAC3C,aACE;AAAA,QAAC;AAAA;AAAA,UAAmB,WAAW,aAAa,kBAAkB;AAAA,UAC1D,OAAO,EAAC,QAAQ,UAAS;AAAA,UAAG,SAAS,MAAM,SAAS,MAAM;AAAA,UAC5D;AAAA,mCAAC,QAAG,WAAU,eACX;AAAA,4BAAc,uBAAC,OAAE,WAAU,yCAAb;AAAA;AAAA;AAAA;AAAA,qBAAkD;AAAA,cAChE,OAAO;AAAA,iBAFV;AAAA;AAAA;AAAA;AAAA,mBAGA;AAAA,YACA,uBAAC,QAAI,iBAAO,YAAZ;AAAA;AAAA;AAAA;AAAA,mBAAqB;AAAA,YACrB,uBAAC,QAAI,wBAAc,OAAO,OAAO,KAAjC;AAAA;AAAA;AAAA;AAAA,mBAAmC;AAAA;AAAA;AAAA,QAP5B,OAAO;AAAA,QAAhB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA;AAAA,IAEJ,CAAC,KAdH;AAAA;AAAA;AAAA;AAAA,WAeA;AAAA,OAvBF;AAAA;AAAA;AAAA;AAAA,SAwBA,KAzBF;AAAA;AAAA;AAAA;AAAA,SA0BA;AAEJ;AAlCwB;", + "names": [] +} diff --git a/js/esm/build/wizard/components/CourseTable.js b/js/esm/build/wizard/components/CourseTable.js new file mode 100644 index 0000000..079dcad --- /dev/null +++ b/js/esm/build/wizard/components/CourseTable.js @@ -0,0 +1,7 @@ +import{str as s}from"../../lang";import{jsx as e,jsxs as o}from"react/jsx-runtime";/** + * A reusable html component that lists cms courses to pick one from. Both import and request use it to show courses. + * + * @module lsf_unification/wizard/components/CourseTable + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */const i=r=>new Date(r*1e3).toLocaleDateString("de-DE",{timeZone:"Europe/Berlin",day:"2-digit",month:"2-digit",year:"numeric"});function m({courses:r,selected:l,onSelect:d}){return r.length===0?e("p",{className:"text-center text-muted py-4 mb-0",children:e("em",{children:s("nocoursesfound")})}):e("div",{className:"table-responsive",children:o("table",{className:"table table-hover table-borderless align-middle mb-0",children:[e("thead",{children:o("tr",{children:[e("th",{scope:"col",children:s("title")}),e("th",{scope:"col",children:s("semester")}),e("th",{scope:"col",children:s("created")})]})}),e("tbody",{children:r.map(t=>{const a=l?.id===t.id;return o("tr",{className:a?"table-primary":"",style:{cursor:"pointer"},onClick:()=>d(t),children:[o("td",{className:"fw-semibold",children:[a&&e("i",{className:"fa-solid fa-check text-primary me-2"}),t.title]}),e("td",{children:t.semester}),e("td",{children:i(t.created)})]},t.id)})})]})})}export{m as default}; diff --git a/js/esm/build/wizard/components/SummaryPanel.dev.js b/js/esm/build/wizard/components/SummaryPanel.dev.js new file mode 100644 index 0000000..6f18d28 --- /dev/null +++ b/js/esm/build/wizard/components/SummaryPanel.dev.js @@ -0,0 +1,106 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { Fragment, jsxDEV } from "react/jsx-dev-runtime"; +/** + * The shared body of both summary steps: it lists what is about to be sent, submits + * it, and reports how that went. Both branches do exactly this and differ only in the + * rows they list, the wording and which service call they make, so those are props. + * + * @module lsf_unification/wizard/components/SummaryPanel + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { Fragment as Fragment2, useState } from "react"; +function SummaryPanel({ rows, submitted, onSubmitted, submit, submitLabel, successText, errorText }) { + const [submitting, setSubmitting] = useState(false); + const [failed, setFailed] = useState(false); + const handleSubmit = /* @__PURE__ */ __name(() => { + setSubmitting(true); + setFailed(false); + submit().then((status) => status ? onSubmitted() : setFailed(true)).catch(() => setFailed(true)).finally(() => setSubmitting(false)); + }, "handleSubmit"); + return /* @__PURE__ */ jsxDEV(Fragment, { children: [ + /* @__PURE__ */ jsxDEV("dl", { className: "row mb-4", children: rows.map(({ label, value }) => /* @__PURE__ */ jsxDEV(Fragment2, { children: [ + /* @__PURE__ */ jsxDEV("dt", { className: "col-sm-4 text-muted fw-normal", children: label }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 59, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("dd", { className: "col-sm-8 fw-semibold", children: value || "\u2013" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 60, + columnNumber: 13 + }, this) + ] }, label, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 58, + columnNumber: 11 + }, this)) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 56, + columnNumber: 7 + }, this), + submitted ? /* @__PURE__ */ jsxDEV("div", { className: "alert alert-success d-flex align-items-center mb-0", role: "alert", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-circle-check me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 67, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV("div", { children: successText }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 68, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 66, + columnNumber: 9 + }, this) : /* @__PURE__ */ jsxDEV(Fragment, { children: [ + failed && /* @__PURE__ */ jsxDEV("div", { className: "alert alert-danger d-flex align-items-center", role: "alert", children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-triangle-exclamation me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 74, + columnNumber: 15 + }, this), + /* @__PURE__ */ jsxDEV("div", { children: errorText }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 75, + columnNumber: 15 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 73, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "d-flex justify-content-end", children: /* @__PURE__ */ jsxDEV("button", { type: "button", className: "btn btn-primary text-white", onClick: handleSubmit, disabled: submitting, children: [ + submitting && /* @__PURE__ */ jsxDEV("span", { className: "spinner-border spinner-border-sm me-2", role: "status" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 80, + columnNumber: 30 + }, this), + submitLabel + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 79, + columnNumber: 13 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 78, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 71, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/components/SummaryPanel.tsx", + lineNumber: 52, + columnNumber: 5 + }, this); +} +__name(SummaryPanel, "SummaryPanel"); +export { + SummaryPanel as default +}; +//# sourceMappingURL=SummaryPanel.dev.js.map diff --git a/js/esm/build/wizard/components/SummaryPanel.dev.js.map b/js/esm/build/wizard/components/SummaryPanel.dev.js.map new file mode 100644 index 0000000..d6ef79e --- /dev/null +++ b/js/esm/build/wizard/components/SummaryPanel.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../src/wizard/components/SummaryPanel.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The shared body of both summary steps: it lists what is about to be sent, submits\n * it, and reports how that went. Both branches do exactly this and differ only in the\n * rows they list, the wording and which service call they make, so those are props.\n *\n * @module lsf_unification/wizard/components/SummaryPanel\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Fragment, useState} from \"react\";\n\ntype Props = {\n rows: {label: string; value: string}[]; /** The cache as label/value pairs to show. */\n submitted: boolean; /** Whether the submit already went through. */\n onSubmitted: () => void; /** Tells the shell the wizard is done. */\n submit: () => Promise; /** Sends the cache; resolves with whether it worked. */\n submitLabel: string; /** Caption of the submit button. */\n successText: string; /** Shown in place of the button once it went through. */\n errorText: string; /** Shown above the button when it did not. */\n};\n\nexport default function SummaryPanel({rows, submitted, onSubmitted, submit, submitLabel, successText, errorText}: Props) {\n const [submitting, setSubmitting] = useState(false);\n const [failed, setFailed] = useState(false);\n\n const handleSubmit = () => {\n setSubmitting(true);\n setFailed(false);\n submit()\n .then((status) => (status ? onSubmitted() : setFailed(true)))\n .catch(() => setFailed(true))\n .finally(() => setSubmitting(false));\n };\n\n return (\n <>\n {/* The dt/dd pairs are the columns of this row, so they must sit in it directly:\n wrapping each pair in a row of its own would nest the gutters and pull the rows\n out to the left of the heading. */}\n
\n {rows.map(({label, value}) => (\n \n
{label}
\n
{value || \"\u2013\"}
\n
\n ))}\n
\n\n {submitted ? (\n
\n \n
{successText}
\n
\n ) : (\n <>\n {failed && (\n
\n \n
{errorText}
\n
\n )}\n
\n \n
\n \n )}\n \n );\n}\n"], + "mappings": ";;AA0DY,SAYJ,UAZI;AA3CZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,SAAQ,YAAAA,WAAU,gBAAe;AAYlB,SAAR,aAA8B,EAAC,MAAM,WAAW,aAAa,QAAQ,aAAa,aAAa,UAAS,GAAU;AACvH,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAE1C,QAAM,eAAe,6BAAM;AACzB,kBAAc,IAAI;AAClB,cAAU,KAAK;AACf,WAAO,EACJ,KAAK,CAAC,WAAY,SAAS,YAAY,IAAI,UAAU,IAAI,CAAE,EAC3D,MAAM,MAAM,UAAU,IAAI,CAAC,EAC3B,QAAQ,MAAM,cAAc,KAAK,CAAC;AAAA,EACvC,GAPqB;AASrB,SACE,mCAIE;AAAA,2BAAC,QAAG,WAAU,YACX,eAAK,IAAI,CAAC,EAAC,OAAO,MAAK,MACtB,uBAACA,WAAA,EACC;AAAA,6BAAC,QAAG,WAAU,iCAAiC,mBAA/C;AAAA;AAAA;AAAA;AAAA,aAAqD;AAAA,MACrD,uBAAC,QAAG,WAAU,wBAAwB,mBAAS,YAA/C;AAAA;AAAA;AAAA;AAAA,aAAmD;AAAA,SAFtC,OAAf;AAAA;AAAA;AAAA;AAAA,WAGA,CACD,KANH;AAAA;AAAA;AAAA;AAAA,WAOA;AAAA,IAEC,YACC,uBAAC,SAAI,WAAU,sDAAqD,MAAK,SACvE;AAAA,6BAAC,OAAE,WAAU,mCAAb;AAAA;AAAA;AAAA;AAAA,aAA4C;AAAA,MAC5C,uBAAC,SAAK,yBAAN;AAAA;AAAA;AAAA;AAAA,aAAkB;AAAA,SAFpB;AAAA;AAAA;AAAA;AAAA,WAGA,IAEA,mCACG;AAAA,gBACC,uBAAC,SAAI,WAAU,gDAA+C,MAAK,SACjE;AAAA,+BAAC,OAAE,WAAU,2CAAb;AAAA;AAAA;AAAA;AAAA,eAAoD;AAAA,QACpD,uBAAC,SAAK,uBAAN;AAAA;AAAA;AAAA;AAAA,eAAgB;AAAA,WAFlB;AAAA;AAAA;AAAA;AAAA,aAGA;AAAA,MAEF,uBAAC,SAAI,WAAU,8BACb,iCAAC,YAAO,MAAK,UAAS,WAAU,8BAA6B,SAAS,cAAc,UAAU,YAC3F;AAAA,sBAAc,uBAAC,UAAK,WAAU,yCAAwC,MAAK,YAA7D;AAAA;AAAA;AAAA;AAAA,eAAqE;AAAA,QACnF;AAAA,WAFH;AAAA;AAAA;AAAA;AAAA,aAGA,KAJF;AAAA;AAAA;AAAA;AAAA,aAKA;AAAA,SAZF;AAAA;AAAA;AAAA;AAAA,WAaA;AAAA,OAhCJ;AAAA;AAAA;AAAA;AAAA,SAkCA;AAEJ;AAlDwB;", + "names": ["Fragment"] +} diff --git a/js/esm/build/wizard/components/SummaryPanel.js b/js/esm/build/wizard/components/SummaryPanel.js new file mode 100644 index 0000000..5b64999 --- /dev/null +++ b/js/esm/build/wizard/components/SummaryPanel.js @@ -0,0 +1,9 @@ +import{Fragment as N,useState as r}from"react";import{Fragment as n,jsx as e,jsxs as t}from"react/jsx-runtime";/** + * The shared body of both summary steps: it lists what is about to be sent, submits + * it, and reports how that went. Both branches do exactly this and differ only in the + * rows they list, the wording and which service call they make, so those are props. + * + * @module lsf_unification/wizard/components/SummaryPanel + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */function x({rows:m,submitted:d,onSubmitted:o,submit:c,submitLabel:u,successText:b,errorText:f}){const[l,i]=r(!1),[g,a]=r(!1),v=()=>{i(!0),a(!1),c().then(s=>s?o():a(!0)).catch(()=>a(!0)).finally(()=>i(!1))};return t(n,{children:[e("dl",{className:"row mb-4",children:m.map(({label:s,value:p})=>t(N,{children:[e("dt",{className:"col-sm-4 text-muted fw-normal",children:s}),e("dd",{className:"col-sm-8 fw-semibold",children:p||"\u2013"})]},s))}),d?t("div",{className:"alert alert-success d-flex align-items-center mb-0",role:"alert",children:[e("i",{className:"fa-solid fa-circle-check me-2"}),e("div",{children:b})]}):t(n,{children:[g&&t("div",{className:"alert alert-danger d-flex align-items-center",role:"alert",children:[e("i",{className:"fa-solid fa-triangle-exclamation me-2"}),e("div",{children:f})]}),e("div",{className:"d-flex justify-content-end",children:t("button",{type:"button",className:"btn btn-primary text-white",onClick:v,disabled:l,children:[l&&e("span",{className:"spinner-border spinner-border-sm me-2",role:"status"}),u]})})]})]})}export{x as default}; diff --git a/js/esm/build/wizard/steps/ChooseActionStep.dev.js b/js/esm/build/wizard/steps/ChooseActionStep.dev.js new file mode 100644 index 0000000..7174112 --- /dev/null +++ b/js/esm/build/wizard/steps/ChooseActionStep.dev.js @@ -0,0 +1,111 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { jsxDEV } from "react/jsx-dev-runtime"; +/** + * Wizard step: the user chooses whether to import an existing course or to request a new course in the name of a teacher. + * This choice decides which branch of steps the wizard shows afterward. + * + * @module lsf_unification/wizard/steps/ChooseActionStep + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { str } from "../../lang"; +function ChooseActionStep({ cache, patch }) { + const selected = cache.branch; + return /* @__PURE__ */ jsxDEV("div", { children: [ + /* @__PURE__ */ jsxDEV("h5", { className: "mb-1", children: str("choose_action") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 33, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "row g-3", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "col-md-6", children: /* @__PURE__ */ jsxDEV( + "button", + { + type: "button", + className: "card h-100 w-100 text-center p-4 border-2" + (selected === "import" ? " border-primary" : ""), + onClick: () => patch({ branch: "import" }), + children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-file-import fa-2x text-primary mb-3" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 41, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("span", { className: "fw-semibold", children: str("submit_import_label") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 42, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("span", { className: "small text-muted mt-1", children: str("choose_action_import") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 43, + columnNumber: 25 + }, this) + ] + }, + void 0, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 37, + columnNumber: 21 + }, + this + ) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 36, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "col-md-6", children: /* @__PURE__ */ jsxDEV( + "button", + { + type: "button", + className: "card h-100 w-100 text-center p-4 border-2" + (selected === "request" ? " border-primary" : ""), + onClick: () => patch({ branch: "request" }), + children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-user-tie fa-2x text-primary mb-3" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 54, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("span", { className: "fw-semibold", children: str("submit_request_label") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 55, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("span", { className: "small text-muted mt-1", children: str("choose_action_request") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 56, + columnNumber: 25 + }, this) + ] + }, + void 0, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 50, + columnNumber: 21 + }, + this + ) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 49, + columnNumber: 17 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 35, + columnNumber: 13 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/ChooseActionStep.tsx", + lineNumber: 32, + columnNumber: 9 + }, this); +} +__name(ChooseActionStep, "ChooseActionStep"); +export { + ChooseActionStep as default +}; +//# sourceMappingURL=ChooseActionStep.dev.js.map diff --git a/js/esm/build/wizard/steps/ChooseActionStep.dev.js.map b/js/esm/build/wizard/steps/ChooseActionStep.dev.js.map new file mode 100644 index 0000000..b02534b --- /dev/null +++ b/js/esm/build/wizard/steps/ChooseActionStep.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../src/wizard/steps/ChooseActionStep.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Wizard step: the user chooses whether to import an existing course or to request a new course in the name of a teacher.\n * This choice decides which branch of steps the wizard shows afterward.\n *\n * @module lsf_unification/wizard/steps/ChooseActionStep\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport type {StepProps} from \"./config\";\nimport {str} from \"../../lang\";\n\nexport default function ChooseActionStep({cache, patch}: StepProps) {\n const selected = cache.branch;\n\n return (\n
\n
{str(\"choose_action\")}
\n\n
\n
\n \n
\n\n
\n \n
\n
\n
\n );\n}\n"], + "mappings": ";;AAgCY;AAjBZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,SAAQ,WAAU;AAEH,SAAR,iBAAkC,EAAC,OAAO,MAAK,GAAc;AAChE,QAAM,WAAW,MAAM;AAEvB,SACI,uBAAC,SACG;AAAA,2BAAC,QAAG,WAAU,QAAQ,cAAI,eAAe,KAAzC;AAAA;AAAA;AAAA;AAAA,WAA2C;AAAA,IAE3C,uBAAC,SAAI,WAAU,WACX;AAAA,6BAAC,SAAI,WAAU,YACX;AAAA,QAAC;AAAA;AAAA,UAAO,MAAK;AAAA,UACL,WAAW,+CACJ,aAAa,WAAW,oBAAoB;AAAA,UACnD,SAAS,MAAM,MAAM,EAAC,QAAQ,SAAQ,CAAC;AAAA,UAC3C;AAAA,mCAAC,OAAE,WAAU,qDAAb;AAAA;AAAA;AAAA;AAAA,mBAA8D;AAAA,YAC9D,uBAAC,UAAK,WAAU,eAAe,cAAI,qBAAqB,KAAxD;AAAA;AAAA;AAAA;AAAA,mBAA0D;AAAA,YAC1D,uBAAC,UAAK,WAAU,yBACX,cAAI,sBAAsB,KAD/B;AAAA;AAAA;AAAA;AAAA,mBAEA;AAAA;AAAA;AAAA,QARJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA,KAVJ;AAAA;AAAA;AAAA;AAAA,aAWA;AAAA,MAEA,uBAAC,SAAI,WAAU,YACX;AAAA,QAAC;AAAA;AAAA,UAAO,MAAK;AAAA,UACL,WAAW,+CACJ,aAAa,YAAY,oBAAoB;AAAA,UACpD,SAAS,MAAM,MAAM,EAAC,QAAQ,UAAS,CAAC;AAAA,UAC5C;AAAA,mCAAC,OAAE,WAAU,kDAAb;AAAA;AAAA;AAAA;AAAA,mBAA2D;AAAA,YAC3D,uBAAC,UAAK,WAAU,eAAe,cAAI,sBAAsB,KAAzD;AAAA;AAAA;AAAA;AAAA,mBAA2D;AAAA,YAC3D,uBAAC,UAAK,WAAU,yBACX,cAAI,uBAAuB,KADhC;AAAA;AAAA;AAAA;AAAA,mBAEA;AAAA;AAAA;AAAA,QARJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA,KAVJ;AAAA;AAAA;AAAA;AAAA,aAWA;AAAA,SAzBJ;AAAA;AAAA;AAAA;AAAA,WA0BA;AAAA,OA7BJ;AAAA;AAAA;AAAA;AAAA,SA8BA;AAER;AApCwB;", + "names": [] +} diff --git a/js/esm/build/wizard/steps/ChooseActionStep.js b/js/esm/build/wizard/steps/ChooseActionStep.js new file mode 100644 index 0000000..15ff797 --- /dev/null +++ b/js/esm/build/wizard/steps/ChooseActionStep.js @@ -0,0 +1,8 @@ +import{str as e}from"../../lang";import{jsx as t,jsxs as a}from"react/jsx-runtime";/** + * Wizard step: the user chooses whether to import an existing course or to request a new course in the name of a teacher. + * This choice decides which branch of steps the wizard shows afterward. + * + * @module lsf_unification/wizard/steps/ChooseActionStep + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */function m({cache:r,patch:s}){const o=r.branch;return a("div",{children:[t("h5",{className:"mb-1",children:e("choose_action")}),a("div",{className:"row g-3",children:[t("div",{className:"col-md-6",children:a("button",{type:"button",className:"card h-100 w-100 text-center p-4 border-2"+(o==="import"?" border-primary":""),onClick:()=>s({branch:"import"}),children:[t("i",{className:"fa-solid fa-file-import fa-2x text-primary mb-3"}),t("span",{className:"fw-semibold",children:e("submit_import_label")}),t("span",{className:"small text-muted mt-1",children:e("choose_action_import")})]})}),t("div",{className:"col-md-6",children:a("button",{type:"button",className:"card h-100 w-100 text-center p-4 border-2"+(o==="request"?" border-primary":""),onClick:()=>s({branch:"request"}),children:[t("i",{className:"fa-solid fa-user-tie fa-2x text-primary mb-3"}),t("span",{className:"fw-semibold",children:e("submit_request_label")}),t("span",{className:"small text-muted mt-1",children:e("choose_action_request")})]})})]})]})}export{m as default}; diff --git a/js/esm/build/wizard/steps/config.dev.js b/js/esm/build/wizard/steps/config.dev.js new file mode 100644 index 0000000..f36c9ae --- /dev/null +++ b/js/esm/build/wizard/steps/config.dev.js @@ -0,0 +1,44 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +/** + * The wizard's data model and step map: what the wizard collects, which steps exist, in what order the steps are. + * This file only wires things together, the steps themselves hold their own fetching, markup and rules. + * + * @module lsf_unification/wizard/steps/config + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import ChooseActionStep from "./ChooseActionStep"; +import ImportCoursesStep, { validate as validateImportCourses } from "./import/ImportCoursesStep"; +import ImportDetailsStep, { validate as validateImportDetails } from "./import/ImportDetailsStep"; +import ImportSummaryStep from "./import/ImportSummaryStep"; +import RequestTeacherStep, { validate as validateRequestTeacher } from "./request/RequestTeacherStep"; +import RequestCoursesStep, { validate as validateRequestCourses } from "./request/RequestCoursesStep"; +import RequestSummaryStep from "./request/RequestSummaryStep"; +const STEPS = { + choose: { component: ChooseActionStep }, + importCourses: { component: ImportCoursesStep, validate: validateImportCourses }, + importDetails: { component: ImportDetailsStep, validate: validateImportDetails }, + importSummary: { component: ImportSummaryStep }, + requestTeacher: { component: RequestTeacherStep, validate: validateRequestTeacher }, + requestCourses: { component: RequestCoursesStep, validate: validateRequestCourses }, + requestSummary: { component: RequestSummaryStep } +}; +const SEQUENCES = { + "import": ["importCourses", "importDetails", "importSummary"], + request: ["requestTeacher", "requestCourses", "requestSummary"] +}; +function sequenceFor(branch) { + return branch === null ? ["choose"] : ["choose", ...SEQUENCES[branch]]; +} +__name(sequenceFor, "sequenceFor"); +function canAdvance(stepId, cache) { + return STEPS[stepId].validate?.(cache) ?? true; +} +__name(canAdvance, "canAdvance"); +export { + STEPS, + canAdvance, + sequenceFor +}; +//# sourceMappingURL=config.dev.js.map diff --git a/js/esm/build/wizard/steps/config.dev.js.map b/js/esm/build/wizard/steps/config.dev.js.map new file mode 100644 index 0000000..c371e2a --- /dev/null +++ b/js/esm/build/wizard/steps/config.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../src/wizard/steps/config.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The wizard's data model and step map: what the wizard collects, which steps exist, in what order the steps are.\n * This file only wires things together, the steps themselves hold their own fetching, markup and rules.\n *\n * @module lsf_unification/wizard/steps/config\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport type {ReactElement} from \"react\";\nimport type {Category, Course, Teacher} from \"../../services/csm\";\nimport ChooseActionStep from \"./ChooseActionStep\";\nimport ImportCoursesStep, {validate as validateImportCourses} from \"./import/ImportCoursesStep\";\nimport ImportDetailsStep, {validate as validateImportDetails} from \"./import/ImportDetailsStep\";\nimport ImportSummaryStep from \"./import/ImportSummaryStep\";\nimport RequestTeacherStep, {validate as validateRequestTeacher} from \"./request/RequestTeacherStep\";\nimport RequestCoursesStep, {validate as validateRequestCourses} from \"./request/RequestCoursesStep\";\nimport RequestSummaryStep from \"./request/RequestSummaryStep\";\n\nexport type Branch = \"import\" | \"request\";\n\n/** Every step the wizard can show. */\nexport type StepId =\n | \"choose\"\n | \"importCourses\"\n | \"importDetails\"\n | \"importSummary\"\n | \"requestTeacher\"\n | \"requestCourses\"\n | \"requestSummary\";\n\n/**\n * Everything the wizard collects, filled in step by step. This is what gets submitted:\n * branch - if it's an import or request\n * course - which course gets imported/requested. Can get edited in the import flow.\n * teacher - only used in requests. The teacher that is being request on behalf to.\n * category - only used in imports. Saves the category the course gets assigned to.\n * submitted - only used for rendering purposes\n */\nexport type WizardCache = {\n branch: Branch | null;\n course: Course | null;\n teacher: Teacher | null;\n category: Category | null;\n submitted: boolean;\n};\n\n/**\n * What every step gets: the cache to read, a way to add to it, and whether to show errors.\n * showError is set once the user pressed Next on an incomplete step, so it marks what is missing.\n */\nexport type StepProps = {\n cache: WizardCache;\n patch: (partial: Partial) => void;\n showErrors: boolean;\n};\n\ntype Step = {\n component: (props: StepProps) => ReactElement;\n /** Blocks Next while it returns false. Each step exports its own rule next to the\n * fields it marks; steps with nothing to fill in leave this out. */\n validate?: (cache: WizardCache) => boolean;\n};\n\n/** Every step: what renders it, and what it requires before the user may move on. */\nexport const STEPS: Record = {\n choose: {component: ChooseActionStep},\n importCourses: {component: ImportCoursesStep, validate: validateImportCourses},\n importDetails: {component: ImportDetailsStep, validate: validateImportDetails},\n importSummary: {component: ImportSummaryStep},\n requestTeacher: {component: RequestTeacherStep, validate: validateRequestTeacher},\n requestCourses: {component: RequestCoursesStep, validate: validateRequestCourses},\n requestSummary: {component: RequestSummaryStep},\n};\n\n/** The ordered steps that follow the \"choose\" step, per branch. */\nconst SEQUENCES: Record = {\n \"import\": [\"importCourses\", \"importDetails\", \"importSummary\"],\n request: [\"requestTeacher\", \"requestCourses\", \"requestSummary\"],\n};\n\n/** Builds the full step sequence for the current branch. Before a branch is chosen the wizard only knows about the \"choose\" step.*/\nexport function sequenceFor(branch: Branch | null): StepId[] {\n return branch === null ? [\"choose\"] : [\"choose\", ...SEQUENCES[branch]];\n}\n\n/**\n * Whether the user may leave the given step with the cache as it currently is. The shell calls this when Next is pressed.\n * false keeps the wizard on the step and makes it mark what is missing.\n */\nexport function canAdvance(stepId: StepId, cache: WizardCache): boolean {\n return STEPS[stepId].validate?.(cache) ?? true;\n}\n"], + "mappings": ";;AAeA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,OAAO,sBAAsB;AAC7B,OAAO,qBAAoB,YAAY,6BAA4B;AACnE,OAAO,qBAAoB,YAAY,6BAA4B;AACnE,OAAO,uBAAuB;AAC9B,OAAO,sBAAqB,YAAY,8BAA6B;AACrE,OAAO,sBAAqB,YAAY,8BAA6B;AACrE,OAAO,wBAAwB;AAgDxB,MAAM,QAA8B;AAAA,EACvC,QAAQ,EAAC,WAAW,iBAAgB;AAAA,EACpC,eAAe,EAAC,WAAW,mBAAmB,UAAU,sBAAqB;AAAA,EAC7E,eAAe,EAAC,WAAW,mBAAmB,UAAU,sBAAqB;AAAA,EAC7E,eAAe,EAAC,WAAW,kBAAiB;AAAA,EAC5C,gBAAgB,EAAC,WAAW,oBAAoB,UAAU,uBAAsB;AAAA,EAChF,gBAAgB,EAAC,WAAW,oBAAoB,UAAU,uBAAsB;AAAA,EAChF,gBAAgB,EAAC,WAAW,mBAAkB;AAClD;AAGA,MAAM,YAAsC;AAAA,EACxC,UAAU,CAAC,iBAAiB,iBAAiB,eAAe;AAAA,EAC5D,SAAS,CAAC,kBAAkB,kBAAkB,gBAAgB;AAClE;AAGO,SAAS,YAAY,QAAiC;AACzD,SAAO,WAAW,OAAO,CAAC,QAAQ,IAAI,CAAC,UAAU,GAAG,UAAU,MAAM,CAAC;AACzE;AAFgB;AAQT,SAAS,WAAW,QAAgB,OAA6B;AACpE,SAAO,MAAM,MAAM,EAAE,WAAW,KAAK,KAAK;AAC9C;AAFgB;", + "names": [] +} diff --git a/js/esm/build/wizard/steps/config.js b/js/esm/build/wizard/steps/config.js new file mode 100644 index 0000000..1908bec --- /dev/null +++ b/js/esm/build/wizard/steps/config.js @@ -0,0 +1,8 @@ +import r from"./ChooseActionStep";import o,{validate as a}from"./import/ImportCoursesStep";import p,{validate as s}from"./import/ImportDetailsStep";import m from"./import/ImportSummaryStep";import i,{validate as c}from"./request/RequestTeacherStep";import u,{validate as n}from"./request/RequestCoursesStep";import l from"./request/RequestSummaryStep";/** + * The wizard's data model and step map: what the wizard collects, which steps exist, in what order the steps are. + * This file only wires things together, the steps themselves hold their own fetching, markup and rules. + * + * @module lsf_unification/wizard/steps/config + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */const d={choose:{component:r},importCourses:{component:o,validate:a},importDetails:{component:p,validate:s},importSummary:{component:m},requestTeacher:{component:i,validate:c},requestCourses:{component:u,validate:n},requestSummary:{component:l}},S={import:["importCourses","importDetails","importSummary"],request:["requestTeacher","requestCourses","requestSummary"]};function R(e){return e===null?["choose"]:["choose",...S[e]]}function T(e,t){return d[e].validate?.(t)??!0}export{d as STEPS,T as canAdvance,R as sequenceFor}; diff --git a/js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js b/js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js new file mode 100644 index 0000000..658d29e --- /dev/null +++ b/js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js @@ -0,0 +1,53 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { jsxDEV } from "react/jsx-dev-runtime"; +/** + * Wizard step: the user picks one of their own cms courses to import into Moodle. + * + * @module lsf_unification/wizard/steps/ImportCoursesStep + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { useEffect, useState } from "react"; +import { fetchOwnCourses } from "../../../services/csm"; +import CourseTable from "../../components/CourseTable"; +import { str } from "../../../lang"; +const validate = /* @__PURE__ */ __name((cache) => cache.course !== null, "validate"); +function ImportCoursesStep({ cache, patch, showErrors }) { + const [courses, setCourses] = useState([]); + useEffect(() => { + fetchOwnCourses().then(setCourses); + }, []); + return /* @__PURE__ */ jsxDEV("div", { children: [ + /* @__PURE__ */ jsxDEV("h5", { className: "mb-1", children: str("courseselect") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportCoursesStep.tsx", + lineNumber: 41, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("p", { className: "text-muted", children: str("courseselect_import") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportCoursesStep.tsx", + lineNumber: 42, + columnNumber: 7 + }, this), + showErrors && !validate(cache) && /* @__PURE__ */ jsxDEV("div", { className: "alert alert-danger py-2", children: str("courseselect_validate") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportCoursesStep.tsx", + lineNumber: 45, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV(CourseTable, { courses, selected: cache.course, onSelect: (course) => patch({ course }) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportCoursesStep.tsx", + lineNumber: 48, + columnNumber: 7 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportCoursesStep.tsx", + lineNumber: 40, + columnNumber: 5 + }, this); +} +__name(ImportCoursesStep, "ImportCoursesStep"); +export { + ImportCoursesStep as default, + validate +}; +//# sourceMappingURL=ImportCoursesStep.dev.js.map diff --git a/js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js.map b/js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js.map new file mode 100644 index 0000000..af2e503 --- /dev/null +++ b/js/esm/build/wizard/steps/import/ImportCoursesStep.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../../src/wizard/steps/import/ImportCoursesStep.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Wizard step: the user picks one of their own cms courses to import into Moodle.\n *\n * @module lsf_unification/wizard/steps/ImportCoursesStep\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {useEffect, useState} from \"react\";\nimport {fetchOwnCourses, Course} from \"../../../services/csm\";\nimport CourseTable from \"../../components/CourseTable\";\nimport type {StepProps, WizardCache} from \"../config\";\nimport {str} from \"../../../lang\";\n\nexport const validate = (cache: WizardCache): boolean => cache.course !== null;\n\nexport default function ImportCoursesStep({cache, patch, showErrors}: StepProps) {\n const [courses, setCourses] = useState([]);\n\n useEffect(() => {\n fetchOwnCourses().then(setCourses);\n }, []);\n\n return (\n
\n
{str(\"courseselect\")}
\n

{str(\"courseselect_import\")}

\n\n {showErrors && !validate(cache) && (\n
{str(\"courseselect_validate\")}
\n )}\n\n patch({course})}/>\n
\n );\n}\n"], + "mappings": ";;AAwCM;AAzBN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,SAAQ,WAAW,gBAAe;AAClC,SAAQ,uBAA8B;AACtC,OAAO,iBAAiB;AAExB,SAAQ,WAAU;AAEX,MAAM,WAAW,wBAAC,UAAgC,MAAM,WAAW,MAAlD;AAET,SAAR,kBAAmC,EAAC,OAAO,OAAO,WAAU,GAAc;AAC/E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAmB,CAAC,CAAC;AAEnD,YAAU,MAAM;AACd,oBAAgB,EAAE,KAAK,UAAU;AAAA,EACnC,GAAG,CAAC,CAAC;AAEL,SACE,uBAAC,SACC;AAAA,2BAAC,QAAG,WAAU,QAAQ,cAAI,cAAc,KAAxC;AAAA;AAAA;AAAA;AAAA,WAA0C;AAAA,IAC1C,uBAAC,OAAE,WAAU,cAAc,cAAI,qBAAqB,KAApD;AAAA;AAAA;AAAA;AAAA,WAAsD;AAAA,IAErD,cAAc,CAAC,SAAS,KAAK,KAC5B,uBAAC,SAAI,WAAU,2BAA2B,cAAI,uBAAuB,KAArE;AAAA;AAAA;AAAA;AAAA,WAAuE;AAAA,IAGzE,uBAAC,eAAY,SAAkB,UAAU,MAAM,QAAQ,UAAU,CAAC,WAAW,MAAM,EAAC,OAAM,CAAC,KAA3F;AAAA;AAAA;AAAA;AAAA,WAA6F;AAAA,OAR/F;AAAA;AAAA;AAAA;AAAA,SASA;AAEJ;AAnBwB;", + "names": [] +} diff --git a/js/esm/build/wizard/steps/import/ImportCoursesStep.js b/js/esm/build/wizard/steps/import/ImportCoursesStep.js new file mode 100644 index 0000000..e964e8f --- /dev/null +++ b/js/esm/build/wizard/steps/import/ImportCoursesStep.js @@ -0,0 +1,7 @@ +import{useEffect as p,useState as l}from"react";import{fetchOwnCourses as m}from"../../../services/csm";import i from"../../components/CourseTable";import{str as r}from"../../../lang";import{jsx as o,jsxs as n}from"react/jsx-runtime";/** + * Wizard step: the user picks one of their own cms courses to import into Moodle. + * + * @module lsf_unification/wizard/steps/ImportCoursesStep + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */const d=e=>e.course!==null;function f({cache:e,patch:s,showErrors:t}){const[c,a]=l([]);return p(()=>{m().then(a)},[]),n("div",{children:[o("h5",{className:"mb-1",children:r("courseselect")}),o("p",{className:"text-muted",children:r("courseselect_import")}),t&&!d(e)&&o("div",{className:"alert alert-danger py-2",children:r("courseselect_validate")}),o(i,{courses:c,selected:e.course,onSelect:u=>s({course:u})})]})}export{f as default,d as validate}; diff --git a/js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js b/js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js new file mode 100644 index 0000000..429f3f7 --- /dev/null +++ b/js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js @@ -0,0 +1,262 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { jsxDEV } from "react/jsx-dev-runtime"; +/** + * Wizard step: the user refines the details of the course to import. Title, short title, semester and description are pre-filled + * from the selected course and can be edited. + * + * @module lsf_unification/wizard/steps/ImportDetailsStep + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { useEffect, useState } from "react"; +import { fetchCategories } from "../../../services/csm"; +import { str } from "../../../lang"; +function missingFields(cache) { + const blank = /* @__PURE__ */ __name((value) => (value ?? "").trim() === "", "blank"); + return { + title: blank(cache.course?.title), + shorttitle: blank(cache.course?.shorttitle), + semester: blank(cache.course?.semester), + category: cache.category === null + }; +} +__name(missingFields, "missingFields"); +const validate = /* @__PURE__ */ __name((cache) => !Object.values(missingFields(cache)).some(Boolean), "validate"); +function ImportDetailsStep({ cache, patch, showErrors }) { + const [categories, setCategories] = useState([]); + const course = cache.course; + const missing = missingFields(cache); + const invalid = /* @__PURE__ */ __name((isMissing) => showErrors && isMissing ? " is-invalid" : "", "invalid"); + useEffect(() => { + fetchCategories().then((categories2) => setCategories(categories2)); + }, []); + const patchCourse = /* @__PURE__ */ __name((partial) => course && patch({ course: { ...course, ...partial } }), "patchCourse"); + return /* @__PURE__ */ jsxDEV("div", { children: [ + /* @__PURE__ */ jsxDEV("h5", { className: "mb-1", children: str("coursedetails_title") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 62, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("p", { className: "text-muted", children: str("coursedetails_text") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 63, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "mb-3", children: [ + /* @__PURE__ */ jsxDEV("label", { className: "form-label", children: [ + str("title"), + /* @__PURE__ */ jsxDEV("span", { className: "text-danger", children: "*" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 66, + columnNumber: 53 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 66, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV( + "input", + { + type: "text", + className: `form-control${invalid(missing.title)}`, + value: course?.title ?? "", + onChange: (e) => patchCourse({ title: e.target.value }) + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 67, + columnNumber: 9 + }, + this + ), + /* @__PURE__ */ jsxDEV("div", { className: "invalid-feedback", children: str("title_validate") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 69, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 65, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "row", children: [ + /* @__PURE__ */ jsxDEV("div", { className: "col-md-6 mb-3", children: [ + /* @__PURE__ */ jsxDEV("label", { className: "form-label", children: [ + str("shorttitle"), + /* @__PURE__ */ jsxDEV("span", { className: "text-danger", children: "*" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 74, + columnNumber: 60 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 74, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV( + "input", + { + type: "text", + className: `form-control${invalid(missing.shorttitle)}`, + value: course?.shorttitle ?? "", + onChange: (e) => patchCourse({ shorttitle: e.target.value }) + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 75, + columnNumber: 11 + }, + this + ), + /* @__PURE__ */ jsxDEV("div", { className: "invalid-feedback", children: str("shorttitle_validate") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 77, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 73, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "col-md-6 mb-3", children: [ + /* @__PURE__ */ jsxDEV("label", { className: "form-label", children: [ + str("semester"), + /* @__PURE__ */ jsxDEV("span", { className: "text-danger", children: "*" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 80, + columnNumber: 58 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 80, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV( + "input", + { + type: "text", + className: `form-control${invalid(missing.semester)}`, + value: course?.semester ?? "", + onChange: (e) => patchCourse({ semester: e.target.value }) + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 81, + columnNumber: 11 + }, + this + ), + /* @__PURE__ */ jsxDEV("div", { className: "invalid-feedback", children: str("semester_validate") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 83, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 79, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 72, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "mb-3", children: [ + /* @__PURE__ */ jsxDEV("label", { className: "form-label", children: str("description") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 88, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV( + "textarea", + { + className: "form-control", + rows: 4, + value: course?.description ?? "", + onChange: (e) => patchCourse({ description: e.target.value }) + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 89, + columnNumber: 9 + }, + this + ) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 87, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "mb-3", children: [ + /* @__PURE__ */ jsxDEV("label", { className: "form-label", children: [ + str("category"), + /* @__PURE__ */ jsxDEV("span", { className: "text-danger", children: "*" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 94, + columnNumber: 56 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 94, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV( + "select", + { + className: `form-select${invalid(missing.category)}`, + value: cache.category?.id ?? "", + onChange: (e) => patch({ category: categories.find((c) => c.id === Number(e.target.value)) ?? null }), + children: [ + /* @__PURE__ */ jsxDEV("option", { value: "", disabled: true, children: str("category_choose") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 97, + columnNumber: 11 + }, this), + categories.map((category) => /* @__PURE__ */ jsxDEV("option", { value: category.id, children: category.name }, category.id, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 99, + columnNumber: 13 + }, this)) + ] + }, + void 0, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 95, + columnNumber: 9 + }, + this + ), + /* @__PURE__ */ jsxDEV("div", { className: "invalid-feedback", children: str("category_validate") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 102, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 93, + columnNumber: 7 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/wizard/steps/import/ImportDetailsStep.tsx", + lineNumber: 61, + columnNumber: 5 + }, this); +} +__name(ImportDetailsStep, "ImportDetailsStep"); +export { + ImportDetailsStep as default, + validate +}; +//# sourceMappingURL=ImportDetailsStep.dev.js.map diff --git a/js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js.map b/js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js.map new file mode 100644 index 0000000..a23626d --- /dev/null +++ b/js/esm/build/wizard/steps/import/ImportDetailsStep.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../../src/wizard/steps/import/ImportDetailsStep.tsx"], + "sourceRoot": "../../../../../../sources/public/local/lsf_unification/js/esm/build", + "sourcesContent": ["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Wizard step: the user refines the details of the course to import. Title, short title, semester and description are pre-filled\n * from the selected course and can be edited.\n *\n * @module lsf_unification/wizard/steps/ImportDetailsStep\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {useEffect, useState} from \"react\";\nimport {fetchCategories, Category, Course} from \"../../../services/csm\";\nimport type {StepProps, WizardCache} from \"../config\";\nimport {str} from \"../../../lang\";\n\n/**\n * Which of the required fields are still empty. The description is optional, everything else is needed to import the course.\n */\nfunction missingFields(cache: WizardCache) {\n const blank = (value?: string) => (value ?? \"\").trim() === \"\";\n return {\n title: blank(cache.course?.title),\n shorttitle: blank(cache.course?.shorttitle),\n semester: blank(cache.course?.semester),\n category: cache.category === null,\n };\n}\n\nexport const validate = (cache: WizardCache): boolean => !Object.values(missingFields(cache)).some(Boolean);\n\nexport default function ImportDetailsStep({cache, patch, showErrors}: StepProps) {\n const [categories, setCategories] = useState([]);\n const course = cache.course;\n const missing = missingFields(cache);\n\n /** A field turns red only after the user tried to move on, and goes back to normal as soon as they fill it in. */\n const invalid = (isMissing: boolean) => (showErrors && isMissing ? \" is-invalid\" : \"\");\n\n useEffect(() => {\n fetchCategories().then((categories) => setCategories(categories));\n }, []);\n\n /** Patches a single field of the course and writes the updated course back to the cache. */\n const patchCourse = (partial: Partial) => course && patch({course: {...course, ...partial}});\n\n return (\n
\n
{str(\"coursedetails_title\")}
\n

{str(\"coursedetails_text\")}

\n\n
\n \n patchCourse({title: e.target.value})}/>\n
{str(\"title_validate\")}
\n
\n\n
\n
\n \n patchCourse({shorttitle: e.target.value})}/>\n
{str(\"shorttitle_validate\")}
\n
\n
\n \n patchCourse({semester: e.target.value})}/>\n
{str(\"semester_validate\")}
\n
\n
\n\n
\n \n