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/local/dto/request_dto.php b/classes/local/dto/request_dto.php new file mode 100644 index 0000000..40ca53f --- /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 $requeststate, + /** @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, + 'requeststate' => $this->requeststate, + 'created' => $this->created, + ]; + } +} diff --git a/classes/local/models/cms.php b/classes/local/models/cms.php new file mode 100644 index 0000000..f2ac464 --- /dev/null +++ b/classes/local/models/cms.php @@ -0,0 +1,29 @@ +. + +namespace local_lsf_unification\local\models; + +/** + * Declares all existing content management system that are used in lsf_unification. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +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..3270cc8 --- /dev/null +++ b/classes/local/models/course_request.php @@ -0,0 +1,130 @@ +. + +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, + /** @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, + 'created' => $this->created, + ]); + } + + /** + * 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, + 'created' => $this->created, + ]; + } +} 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..0b626b6 --- /dev/null +++ b/classes/local/service/cms_service.php @@ -0,0 +1,154 @@ +. + +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, + 'created' => time(), + ]; + $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..842775e --- /dev/null +++ b/classes/local/service/dashboard_service.php @@ -0,0 +1,174 @@ +. + +namespace local_lsf_unification\local\service; + +use Exception; +use coding_exception; +use core\context\system; +use core_user; +use local_lsf_unification\local\models\course_request; +use local_lsf_unification\local\dto\course_dto; +use local_lsf_unification\local\dto\request_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 the requests that users made for a course where the current user is the teacher of. + * @return array + */ + public static function get_course_requests(): array { + global $DB, $USER; + $mintime = time() - (get_config('local_lsf_unification', 'max_import_age') * DAYSECS); + // Get courses that were requested on the users behalf. + $sql = "SELECT request.id AS requestid, request.state AS requeststate, request.created AS requestcreated, course.*, + u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic, u.middlename, u.alternatename + FROM {local_lsf_unification_course_requests} request + JOIN {local_lsf_unification_courses} course ON course.id = request.courseid + JOIN {user} u ON u.id = request.requesterid + WHERE course.teacher = :username + AND course.created > :mintime + AND request.state = :pendingstate + ORDER BY course.created DESC"; + $params = ['username' => $USER->username, 'mintime' => $mintime, 'pendingstate' => course_request::REQUEST_PENDING]; + $records = $DB->get_records_sql($sql, $params); + + return array_map( + fn(object $record) => new request_dto( + $record->requestid, + $record->title, + core_user::get_fullname( + (object) [ + 'firstname' => $record->firstname, + 'lastname' => $record->lastname, + 'firstnamephonetic' => $record->firstnamephonetic, + 'lastnamephonetic' => $record->lastnamephonetic, + 'middlename' => $record->middlename, + 'alternatename' => $record->alternatename, + ], + system::instance() + ), + $record->requeststate, + $record->requestcreated, + ), + array_values($records), + ); + } + + /** + * Updates an existing request state. + * @param int $id + * @param string $action + * @return bool + * @throws \dml_exception + * @throws \dml_transaction_exception + */ + public static function update_request(int $id, string $action): bool { + global $DB; + + // 1. Step: Validations: Check that the request really exists. Nothing is updated if it does not. + if (empty($DB->get_record(course_request::DB_TABLE_NAME, ["id" => $id]))) { + return false; + } + + try { + $transaction = $DB->start_delegated_transaction(); + $param = ['id' => $id]; + match ($action) { + 'approve' => $DB->set_field(course_request::DB_TABLE_NAME, 'state', course_request::REQUEST_ACCEPTED, $param), + 'reject' => $DB->set_field(course_request::DB_TABLE_NAME, 'state', course_request::REQUEST_DECLINED, $param), + default => throw new coding_exception('Wrong action parameter given from the frontend'), + }; + $transaction->allow_commit(); + return true; + } catch (Exception $e) { + $transaction->rollback($e); + } + return false; + } + + /** + * 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/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/classes/route/api/dashboard_controller.php b/classes/route/api/dashboard_controller.php new file mode 100644 index 0000000..eaef1c6 --- /dev/null +++ b/classes/route/api/dashboard_controller.php @@ -0,0 +1,262 @@ +. + +/** + * 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\dto\request_dto; +use local_lsf_unification\local\service\cms_service; +use local_lsf_unification\local\service\dashboard_service; +use local_lsf_unification\route\schema\requestbodies\requestaction_body; +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_courserequests_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 requests for a teachers course that was made from other users. The teacher can manage then manage the requests. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + */ + #[route( + title: 'Get Dashboard requests', + description: "Returns the course requests for the current users courses", + path: '/dashboard/dashboardrequests', + method: ['GET'], + responses: [new dashboard_courserequests_response()], + )] + public function get_requests(ServerRequestInterface $request, ResponseInterface $response): payload_response { + $payload = array_map( + fn(request_dto $dto) => $dto->to_array(), + dashboard_service::get_course_requests() + ); + 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); + } + + /** + * Saves a teachers action regarding a request to a teachers course. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return payload_response + * @throws invalid_parameter_exception + */ + #[route( + title: 'Save a course import', + description: 'Stores a course import that was built in the wizard.', + path: '/dashboard/requestaction', + method: ['POST'], + requestbody: new requestaction_body(), + responses: [new submit_response()], + )] + public function post_requestaction(ServerRequestInterface $request, ResponseInterface $response): payload_response { + $requestaction = $request->getParsedBody(); + $status = dashboard_service::update_request($requestaction['id'], $requestaction['action']); + 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..4a1758b --- /dev/null +++ b/classes/route/schema/course_schema.php @@ -0,0 +1,78 @@ +. + +namespace local_lsf_unification\route\schema; + +use core\exception\coding_exception; +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 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/request_schema.php b/classes/route/schema/request_schema.php new file mode 100644 index 0000000..6e24890 --- /dev/null +++ b/classes/route/schema/request_schema.php @@ -0,0 +1,46 @@ +. + +namespace local_lsf_unification\route\schema; + +use core\exception\coding_exception; +use core\param; +use core\router\schema\objects\schema_object; +use core\router\schema\objects\scalar_type; + +/** + * Schema of the request dto. + * + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class request_schema extends schema_object { + /** + * Constructor. + * @throws coding_exception + */ + public function __construct() { + parent::__construct( + content: [ + 'id' => new scalar_type(param::INT), + 'title' => new scalar_type(param::RAW), + 'requester' => new scalar_type(param::RAW), + 'requeststate' => new scalar_type(param::INT), + ] + ); + } +} diff --git a/classes/route/schema/requestbodies/requestaction_body.php b/classes/route/schema/requestbodies/requestaction_body.php new file mode 100644 index 0000000..e620212 --- /dev/null +++ b/classes/route/schema/requestbodies/requestaction_body.php @@ -0,0 +1,50 @@ +. + +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; + +/** + * Request body for request action. + * @package local_lsf_unification + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class requestaction_body extends request_body{ + /** + * Constructor. + * @throws \core\exception\coding_exception + */ + public function __construct() { + parent::__construct( + description: 'The request action: the id of the request that gets updated and the action (approved/rejected)', + content: new payload_response_type( + schema: new schema_object( + content: [ + 'id' => new scalar_type(param::INT, required: true), + 'action' => new scalar_type(param::RAW, required: true), + ], + ), + ), + required: true, + ); + } +} 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_courserequests_response.php b/classes/route/schema/responses/dashboard_courserequests_response.php new file mode 100644 index 0000000..e4dbc6f --- /dev/null +++ b/classes/route/schema/responses/dashboard_courserequests_response.php @@ -0,0 +1,50 @@ +. + +namespace local_lsf_unification\route\schema\responses; + +use core\exception\coding_exception; +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\request_schema; + +/** + * Response specification of the array of requests a teacher can manage 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_courserequests_response extends response { + /** + * Constructor. + * @throws coding_exception + */ + public function __construct() { + parent::__construct( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new array_of_things( + thingtype: new request_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/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/db/install.xml b/db/install.xml index b0da12b..65da189 100644 --- a/db/install.xml +++ b/db/install.xml @@ -20,7 +20,7 @@ - +
@@ -32,7 +32,7 @@
- +
@@ -47,5 +47,51 @@
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + +
\ No newline at end of file diff --git a/db/upgrade.php b/db/upgrade.php index 6493dde..21d9778 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -134,5 +134,84 @@ 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'); + } + + 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'); + $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']); + $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/js/esm/build/Dashboard.dev.js b/js/esm/build/Dashboard.dev.js new file mode 100644 index 0000000..b440f09 --- /dev/null +++ b/js/esm/build/Dashboard.dev.js @@ -0,0 +1,294 @@ +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"; +import RequestManager from "./request_manager/RequestManager"; +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 [requestManagerOpen, setRequestManagerOpen] = 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: 76, + 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: 77, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 75, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV("div", { children: [ + /* @__PURE__ */ jsxDEV("button", { type: "button", className: "btn btn-primary btn-md text-white me-4", onClick: () => setRequestManagerOpen(true), children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-circle-exclamation me-2" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 81, + columnNumber: 13 + }, this), + str("dashboard_request_manager_button") + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 80, + columnNumber: 11 + }, 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: 84, + columnNumber: 13 + }, this), + str("dashboard_wizard_button") + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 83, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 79, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 74, + 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: 91, + 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: 92, + 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: 93, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 90, + 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: 99, + 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: 101, + columnNumber: 19 + }, this), + course.semester + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 100, + columnNumber: 17 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 98, + 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: 105, + columnNumber: 17 + }, this), + str("dashboard_to_course") + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 104, + columnNumber: 15 + }, this) + ] }, course.id, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 97, + columnNumber: 13 + }, this)) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 95, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 89, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("hr", { className: "hr" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 111, + 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: 115, + 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: 116, + 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: 117, + columnNumber: 11 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 114, + 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: 126, + 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: 128, + columnNumber: 19 + }, this), + str("dashboard_request_teacher"), + " ", + course.teacher + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 127, + columnNumber: 17 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 125, + 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: 133, + columnNumber: 21 + }, this), + str(badge.key) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 132, + columnNumber: 17 + }, this) + ] + }, + course.id, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 123, + columnNumber: 13 + }, + this + ); + }) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 119, + columnNumber: 9 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 113, + columnNumber: 7 + }, this), + requestManagerOpen && /* @__PURE__ */ jsxDEV(RequestManager, { onClose: () => { + setRequestManagerOpen(false); + reloadCourses(); + } }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 142, + columnNumber: 30 + }, this), + wizardOpen && /* @__PURE__ */ jsxDEV(Wizard, { onClose: () => { + setWizardOpen(false); + reloadCourses(); + } }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 147, + columnNumber: 22 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/Dashboard.tsx", + lineNumber: 73, + 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..133cb52 --- /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\";\nimport RequestManager from \"./request_manager/RequestManager\";\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 [requestManagerOpen, setRequestManagerOpen] = 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 \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 {/* Request manager modal*/}\n {requestManagerOpen && {\n setRequestManagerOpen(false);\n reloadCourses();\n }}/>}\n {/* Import wizard modal*/}\n {wizardOpen && {\n setWizardOpen(false);\n reloadCourses();\n }}/>}\n \n );\n}"], + "mappings": "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", + "names": [] +} diff --git a/js/esm/build/Dashboard.js b/js/esm/build/Dashboard.js new file mode 100644 index 0000000..9d74826 --- /dev/null +++ b/js/esm/build/Dashboard.js @@ -0,0 +1,6 @@ +import{useEffect as p,useState as d}from"react";import{fetchDashboardCourses as h}from"./services/csm";import g from"./wizard/Wizard";import{str as t}from"./lang";import N from"./request_manager/RequestManager";import{Fragment as q,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 v=1,_=2,x=3,y=4,w={[v]:{css:"text-bg-warning",icon:"fa-regular fa-hourglass-half",key:"dashboard_request_state_pending"},[_]:{css:"text-bg-success",icon:"fa-regular fa-circle-check",key:"dashboard_request_state_accepted"},[x]:{css:"text-bg-danger",icon:"fa-regular fa-circle-xmark",key:"dashboard_request_state_declined"},[y]:{css:"text-bg-primary",icon:"fa-solid fa-file-import",key:"dashboard_request_state_imported"}};function E(){const[l,u]=d([]),[b,o]=d(!1),[f,m]=d(!1),i=()=>h().then(u);p(()=>{i()},[]);const c=l.filter(s=>s.requeststate!==0),n=l.filter(s=>s.moodleid!==0);return a(q,{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("div",{children:[a("button",{type:"button",className:"btn btn-primary btn-md text-white me-4",onClick:()=>m(!0),children:[e("i",{className:"fa-solid fa-circle-exclamation me-2"}),t("dashboard_request_manager_button")]}),a("button",{type:"button",className:"btn btn-primary btn-md text-white",onClick:()=>o(!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:n.length})]}),e("div",{className:"list-group list-group-flush",children:n.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=w[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)})})]}),f&&e(N,{onClose:()=>{m(!1),i()}}),b&&e(g,{onClose:()=>{o(!1),i()}})]})}export{E as default}; diff --git a/js/esm/build/lang.dev.js b/js/esm/build/lang.dev.js new file mode 100644 index 0000000..7c6293e --- /dev/null +++ b/js/esm/build/lang.dev.js @@ -0,0 +1,22 @@ +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, fromCore, param) => { + return fromCore ? M.util.get_string(key, "core", 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..09f3004 --- /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 fromCore if the string is from the lsf language file or the moodle core\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, fromCore?: boolean, param?: StringParams): string => {\n return fromCore ? M.util.get_string(key, \"core\", param) : M.util.get_string(key, \"local_lsf_unification\", param);\n};\n"], + "mappings": ";;AAeA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCO,MAAM,MAAM,wBAAC,KAAa,UAAoB,UAAiC;AAClF,SAAO,WAAW,EAAE,KAAK,WAAW,KAAK,QAAQ,KAAK,IAAI,EAAE,KAAK,WAAW,KAAK,yBAAyB,KAAK;AACnH,GAFmB;", + "names": [] +} diff --git a/js/esm/build/lang.js b/js/esm/build/lang.js new file mode 100644 index 0000000..629a80b --- /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 i=(t,n,r)=>n?M.util.get_string(t,"core",r):M.util.get_string(t,"local_lsf_unification",r);export{i as str}; diff --git a/js/esm/build/request_manager/RequestActions.dev.js b/js/esm/build/request_manager/RequestActions.dev.js new file mode 100644 index 0000000..c39b7be --- /dev/null +++ b/js/esm/build/request_manager/RequestActions.dev.js @@ -0,0 +1,148 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); +import { jsxDEV } from "react/jsx-dev-runtime"; +/** + * React component for the request manager actions + * @module lsf_unification/RequestActions + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { useState } from "react"; +import { str } from "../lang"; +function RequestActions({ request, onDecide }) { + const [confirm, setConfirm] = useState(null); + const [busy, setBusy] = useState(false); + const decide = /* @__PURE__ */ __name(async (action) => { + setBusy(true); + try { + await onDecide({ id: request.id, action }); + } finally { + setBusy(false); + } + }, "decide"); + if (busy) { + return /* @__PURE__ */ jsxDEV( + "span", + { + className: "spinner-border spinner-border-sm text-muted", + role: "status", + "aria-label": str("loading") + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 47, + columnNumber: 12 + }, + this + ); + } + if (confirm) { + return /* @__PURE__ */ jsxDEV("div", { className: "d-flex gap-2 align-items-center", children: [ + /* @__PURE__ */ jsxDEV("span", { className: "small text-muted", children: str(confirm === "approve" ? "confirm_approve" : "confirm_reject") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 54, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV( + "button", + { + type: "button", + autoFocus: true, + className: `btn btn-sm ${confirm === "approve" ? "btn-success" : "btn-danger"}`, + onClick: () => decide(confirm), + children: str("yes", true) + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 57, + columnNumber: 11 + }, + this + ), + /* @__PURE__ */ jsxDEV( + "button", + { + type: "button", + className: "btn btn-sm btn-outline-secondary", + onClick: () => setConfirm(null), + children: str("cancel", true) + }, + void 0, + false, + { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 62, + columnNumber: 11 + }, + this + ) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 53, + columnNumber: 9 + }, this); + } + return /* @__PURE__ */ jsxDEV("div", { className: "d-flex gap-2", children: [ + /* @__PURE__ */ jsxDEV( + "button", + { + type: "button", + className: "btn btn-sm btn-outline-success", + onClick: () => setConfirm("approve"), + children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-check me-1" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 74, + columnNumber: 11 + }, this), + str("approve", true) + ] + }, + void 0, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 72, + columnNumber: 9 + }, + this + ), + /* @__PURE__ */ jsxDEV( + "button", + { + type: "button", + className: "btn btn-sm btn-outline-danger", + onClick: () => setConfirm("reject"), + children: [ + /* @__PURE__ */ jsxDEV("i", { className: "fa-solid fa-xmark me-1" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 78, + columnNumber: 11 + }, this), + str("reject", true) + ] + }, + void 0, + true, + { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 76, + columnNumber: 9 + }, + this + ) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestActions.tsx", + lineNumber: 71, + columnNumber: 7 + }, this); +} +__name(RequestActions, "RequestActions"); +export { + RequestActions as default +}; +//# sourceMappingURL=RequestActions.dev.js.map diff --git a/js/esm/build/request_manager/RequestActions.dev.js.map b/js/esm/build/request_manager/RequestActions.dev.js.map new file mode 100644 index 0000000..653e522 --- /dev/null +++ b/js/esm/build/request_manager/RequestActions.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../src/request_manager/RequestActions.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 for the request manager actions\n * @module lsf_unification/RequestActions\n * @copyright 2026 Tamaro Walter\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {useState} from \"react\";\nimport {str} from \"../lang\";\nimport type {Request, RequestAction, RequestActionType} from \"../services/csm\";\n\n\ntype Props = {\n request: Request;\n onDecide: (action: RequestAction) => Promise;\n};\n\nexport default function RequestActions({request, onDecide}: Props) {\n const [confirm, setConfirm] = useState(null);\n const [busy, setBusy] = useState(false);\n\n const decide = async (action: RequestActionType) => {\n setBusy(true);\n try {\n await onDecide({id: request.id, action});\n } finally {\n setBusy(false);\n }\n };\n\n if (busy) {\n return ;\n }\n\n if (confirm) {\n return (\n
\n \n {str(confirm === \"approve\" ? \"confirm_approve\" : \"confirm_reject\")}\n \n \n \n
\n );\n }\n\n return (\n
\n \n \n
\n );\n}"], + "mappings": ";;AA8CW;AA/BX;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,SAAQ,gBAAe;AACvB,SAAQ,WAAU;AASH,SAAR,eAAgC,EAAC,SAAS,SAAQ,GAAU;AACjE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAmC,IAAI;AACrE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAEtC,QAAM,SAAS,8BAAO,WAA8B;AAClD,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,SAAS,EAAC,IAAI,QAAQ,IAAI,OAAM,CAAC;AAAA,IACzC,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAPe;AASf,MAAI,MAAM;AACR,WAAO;AAAA,MAAC;AAAA;AAAA,QAAK,WAAU;AAAA,QAA8C,MAAK;AAAA,QAC7D,cAAY,IAAI,SAAS;AAAA;AAAA,MAD/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IACiC;AAAA,EAC1C;AAEA,MAAI,SAAS;AACX,WACI,uBAAC,SAAI,WAAU,mCACf;AAAA,6BAAC,UAAK,WAAU,oBACb,cAAI,YAAY,YAAY,oBAAoB,gBAAgB,KADnE;AAAA;AAAA;AAAA;AAAA,aAEA;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UAAO,MAAK;AAAA,UAAS,WAAS;AAAA,UACvB,WAAW,cAAc,YAAY,YAAY,gBAAgB,YAAY;AAAA,UAC7E,SAAS,MAAM,OAAO,OAAO;AAAA,UAClC,cAAI,OAAO,IAAI;AAAA;AAAA,QAHlB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAIA;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UAAO,MAAK;AAAA,UAAS,WAAU;AAAA,UACxB,SAAS,MAAM,WAAW,IAAI;AAAA,UACnC,cAAI,UAAU,IAAI;AAAA;AAAA,QAFrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAGA;AAAA,SAZF;AAAA;AAAA;AAAA;AAAA,WAaA;AAAA,EAEN;AAEA,SACI,uBAAC,SAAI,WAAU,gBACb;AAAA;AAAA,MAAC;AAAA;AAAA,QAAO,MAAK;AAAA,QAAS,WAAU;AAAA,QACxB,SAAS,MAAM,WAAW,SAAS;AAAA,QACzC;AAAA,iCAAC,OAAE,WAAU,4BAAb;AAAA;AAAA;AAAA;AAAA,iBAAqC;AAAA,UAAG,IAAI,WAAW,IAAI;AAAA;AAAA;AAAA,MAF7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QAAO,MAAK;AAAA,QAAS,WAAU;AAAA,QACxB,SAAS,MAAM,WAAW,QAAQ;AAAA,QACxC;AAAA,iCAAC,OAAE,WAAU,4BAAb;AAAA;AAAA;AAAA;AAAA,iBAAqC;AAAA,UAAG,IAAI,UAAU,IAAI;AAAA;AAAA;AAAA,MAF5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAGA;AAAA,OARF;AAAA;AAAA;AAAA;AAAA,SASA;AAEN;AAjDwB;", + "names": [] +} diff --git a/js/esm/build/request_manager/RequestActions.js b/js/esm/build/request_manager/RequestActions.js new file mode 100644 index 0000000..1874cd9 --- /dev/null +++ b/js/esm/build/request_manager/RequestActions.js @@ -0,0 +1,6 @@ +import{useState as r}from"react";import{str as t}from"../lang";import{jsx as e,jsxs as n}from"react/jsx-runtime";/** + * React component for the request manager actions + * @module lsf_unification/RequestActions + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */function p({request:c,onDecide:i}){const[s,o]=r(null),[u,a]=r(!1),l=async m=>{a(!0);try{await i({id:c.id,action:m})}finally{a(!1)}};return u?e("span",{className:"spinner-border spinner-border-sm text-muted",role:"status","aria-label":t("loading")}):s?n("div",{className:"d-flex gap-2 align-items-center",children:[e("span",{className:"small text-muted",children:t(s==="approve"?"confirm_approve":"confirm_reject")}),e("button",{type:"button",autoFocus:!0,className:`btn btn-sm ${s==="approve"?"btn-success":"btn-danger"}`,onClick:()=>l(s),children:t("yes",!0)}),e("button",{type:"button",className:"btn btn-sm btn-outline-secondary",onClick:()=>o(null),children:t("cancel",!0)})]}):n("div",{className:"d-flex gap-2",children:[n("button",{type:"button",className:"btn btn-sm btn-outline-success",onClick:()=>o("approve"),children:[e("i",{className:"fa-solid fa-check me-1"}),t("approve",!0)]}),n("button",{type:"button",className:"btn btn-sm btn-outline-danger",onClick:()=>o("reject"),children:[e("i",{className:"fa-solid fa-xmark me-1"}),t("reject",!0)]})]})}export{p as default}; diff --git a/js/esm/build/request_manager/RequestManager.dev.js b/js/esm/build/request_manager/RequestManager.dev.js new file mode 100644 index 0000000..0c6e756 --- /dev/null +++ b/js/esm/build/request_manager/RequestManager.dev.js @@ -0,0 +1,167 @@ +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 request manager. + * @module lsf_unification/RequestManager + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { useEffect, useState } from "react"; +import { fetchRequests, submitRequestAction } from "../services/csm"; +import RequestActions from "./RequestActions"; +import { str } from "../lang"; +const formatDate = /* @__PURE__ */ __name((date) => new Date(date * 1e3).toLocaleDateString( + "de-DE", + { timeZone: "Europe/Berlin", day: "2-digit", month: "2-digit", year: "numeric" } +), "formatDate"); +function RequestManager({ onClose }) { + const [requests, setRequests] = useState([]); + const handleDecide = /* @__PURE__ */ __name(async (action) => { + await submitRequestAction(action); + setRequests(await fetchRequests()); + }, "handleDecide"); + useEffect(() => { + fetchRequests().then(setRequests); + const onKey = /* @__PURE__ */ __name((e) => e.key === "Escape" && onClose(), "onKey"); + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [onClose]); + 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: str("request_manager_title") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 54, + 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/request_manager/RequestManager.tsx", + lineNumber: 55, + columnNumber: 15 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 53, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "modal-body", children: /* @__PURE__ */ jsxDEV("div", { children: [ + /* @__PURE__ */ jsxDEV("p", { className: "text-muted", children: str("request_manager_text") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 59, + columnNumber: 17 + }, this), + /* @__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("course") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 64, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("th", { scope: "col", children: str("user") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 65, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("th", { scope: "col", children: str("created") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 66, + columnNumber: 25 + }, this), + /* @__PURE__ */ jsxDEV("th", { scope: "col", children: str("action") }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 67, + columnNumber: 25 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 63, + columnNumber: 23 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 62, + columnNumber: 21 + }, this), + /* @__PURE__ */ jsxDEV("tbody", { children: requests.map((request) => { + return /* @__PURE__ */ jsxDEV("tr", { children: [ + /* @__PURE__ */ jsxDEV("td", { className: "fw-semibold", children: request.title }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 74, + columnNumber: 27 + }, this), + /* @__PURE__ */ jsxDEV("td", { children: request.requester }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 77, + columnNumber: 27 + }, this), + /* @__PURE__ */ jsxDEV("td", { children: formatDate(request.created) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 78, + columnNumber: 27 + }, this), + /* @__PURE__ */ jsxDEV("td", { children: /* @__PURE__ */ jsxDEV(RequestActions, { request, onDecide: handleDecide }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 79, + columnNumber: 31 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 79, + columnNumber: 27 + }, this) + ] }, request.id, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 73, + columnNumber: 25 + }, this); + }) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 70, + columnNumber: 21 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 61, + columnNumber: 19 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 60, + columnNumber: 17 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 58, + columnNumber: 15 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 57, + columnNumber: 13 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 52, + columnNumber: 11 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 51, + columnNumber: 9 + }, this) }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 50, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV("div", { className: "modal-backdrop fade show" }, void 0, false, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 91, + columnNumber: 7 + }, this) + ] }, void 0, true, { + fileName: "public/local/lsf_unification/js/esm/src/request_manager/RequestManager.tsx", + lineNumber: 49, + columnNumber: 5 + }, this); +} +__name(RequestManager, "RequestManager"); +export { + RequestManager as default +}; +//# sourceMappingURL=RequestManager.dev.js.map diff --git a/js/esm/build/request_manager/RequestManager.dev.js.map b/js/esm/build/request_manager/RequestManager.dev.js.map new file mode 100644 index 0000000..3c614ac --- /dev/null +++ b/js/esm/build/request_manager/RequestManager.dev.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../src/request_manager/RequestManager.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 request manager.\n * @module lsf_unification/RequestManager\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 {fetchRequests, Request, RequestAction, submitRequestAction} from \"../services/csm\";\nimport RequestActions from \"./RequestActions\";\nimport {str} from \"../lang\";\n\nconst formatDate = (date: number): string =>\n new Date(date * 1000).toLocaleDateString(\"de-DE\",\n {timeZone: \"Europe/Berlin\", day: \"2-digit\", month: \"2-digit\", year: \"numeric\"});\n\nexport default function RequestManager({onClose} : {onClose: () => void}) {\n const [requests, setRequests] = useState([]);\n\n const handleDecide = async(action: RequestAction) => {\n await submitRequestAction(action);\n setRequests(await fetchRequests());\n };\n\n useEffect(() => {\n fetchRequests().then(setRequests);\n\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && onClose();\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [onClose]);\n\n return (\n <>\n
\n
\n
\n
\n
{str(\"request_manager_title\")}
\n
\n
\n
\n

{str(\"request_manager_text\")}

\n
\n \n \n \n \n \n \n \n \n \n \n {requests.map((request) => {\n return (\n \n \n \n \n \n \n );\n })}\n \n
{str(\"course\")}{str(\"user\")}{str(\"created\")}{str(\"action\")}
\n {request.title}\n {request.requester}{formatDate(request.created)}
\n
\n
\n
\n
\n
\n
\n
\n \n );\n}\n"], + "mappings": "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", + "names": [] +} diff --git a/js/esm/build/request_manager/RequestManager.js b/js/esm/build/request_manager/RequestManager.js new file mode 100644 index 0000000..e174a25 --- /dev/null +++ b/js/esm/build/request_manager/RequestManager.js @@ -0,0 +1,6 @@ +import{useEffect as n,useState as m}from"react";import{fetchRequests as i,submitRequestAction as u}from"../services/csm";import b from"./RequestActions";import{str as a}from"../lang";import{Fragment as p,jsx as e,jsxs as d}from"react/jsx-runtime";/** + * React component that shows the request manager. + * @module lsf_unification/RequestManager + * @copyright 2026 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */const v=o=>new Date(o*1e3).toLocaleDateString("de-DE",{timeZone:"Europe/Berlin",day:"2-digit",month:"2-digit",year:"numeric"});function h({onClose:o}){const[l,s]=m([]),r=async t=>{await u(t),s(await i())};return n(()=>{i().then(s);const t=c=>c.key==="Escape"&&o();return document.addEventListener("keydown",t),()=>document.removeEventListener("keydown",t)},[o]),d(p,{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:d("div",{className:"modal-content shadow",children:[d("div",{className:"modal-header",children:[e("h5",{className:"modal-title",children:a("request_manager_title")}),e("button",{type:"button",className:"btn-close","aria-label":a("close"),onClick:o})]}),e("div",{className:"modal-body",children:d("div",{children:[e("p",{className:"text-muted",children:a("request_manager_text")}),e("div",{className:"table-responsive",children:d("table",{className:"table table-hover table-borderless align-middle mb-0",children:[e("thead",{children:d("tr",{children:[e("th",{scope:"col",children:a("course")}),e("th",{scope:"col",children:a("user")}),e("th",{scope:"col",children:a("created")}),e("th",{scope:"col",children:a("action")})]})}),e("tbody",{children:l.map(t=>d("tr",{children:[e("td",{className:"fw-semibold",children:t.title}),e("td",{children:t.requester}),e("td",{children:v(t.created)}),e("td",{children:e(b,{request:t,onDecide:r})})]},t.id))})]})})]})})]})})}),e("div",{className:"modal-backdrop fade show"})]})}export{h as default}; diff --git a/js/esm/build/services/csm.dev.js b/js/esm/build/services/csm.dev.js new file mode 100644 index 0000000..fc5f62f --- /dev/null +++ b/js/esm/build/services/csm.dev.js @@ -0,0 +1,54 @@ +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 fetchRequests = /* @__PURE__ */ __name(async () => { + const response = await Fetch.performGet("local_lsf_unification", "dashboard/dashboardrequests"); + return await response.json(); +}, "fetchRequests"); +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 submitRequestAction = /* @__PURE__ */ __name(async (action) => { + const response = await Fetch.performPost("local_lsf_unification", "dashboard/requestaction", { body: action }); + const payload = await response.json(); + return payload.status; +}, "submitRequestAction"); +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, + fetchRequests, + fetchTeacherCourses, + fetchTeachers, + submitCourseImport, + submitCourseRequest, + submitRequestAction +}; +//# 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..2841771 --- /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 Request = {\n id: number;\n title: string;\n requester: string;\n requeststate: number;\n created: number;\n};\n\nexport type RequestActionType = \"approve\" | \"reject\";\n\nexport type RequestAction = {\n id: number;\n action: RequestActionType;\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\nexport const fetchRequests = async(): Promise => {\n const response = await Fetch.performGet(\"local_lsf_unification\", \"dashboard/dashboardrequests\");\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 submitRequestAction = async(action: RequestAction): Promise => {\n const response = await Fetch.performPost(\"local_lsf_unification\", \"dashboard/requestaction\", {body: action});\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;AA8CX,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;AAK5B,MAAM,gBAAgB,mCAA+B;AACxD,QAAM,WAAW,MAAM,MAAM,WAAW,yBAAyB,6BAA6B;AAC9F,SAAO,MAAM,SAAS,KAAK;AAC/B,GAH6B;AAStB,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,sBAAsB,8BAAM,WAA4C;AACjF,QAAM,WAAW,MAAM,MAAM,YAAY,yBAAyB,2BAA2B,EAAC,MAAM,OAAM,CAAC;AAC3G,QAAM,UAAU,MAAM,SAAS,KAAK;AACpC,SAAO,QAAQ;AACnB,GAJmC;AAM5B,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..b282a6e --- /dev/null +++ b/js/esm/build/services/csm.js @@ -0,0 +1 @@ +import s from"@moodle/lms/core/fetch";const a=async()=>await(await s.performGet("local_lsf_unification","dashboard/dashboardcourses")).json(),n=async()=>await(await s.performGet("local_lsf_unification","dashboard/courses")).json(),i=async e=>await(await s.performGet("local_lsf_unification",`dashboard/courses/${e}`)).json(),c=async()=>await(await s.performGet("local_lsf_unification","dashboard/dashboardrequests")).json(),u=async e=>(await(await s.performPost("local_lsf_unification","dashboard/request",{body:e})).json()).status,p=async e=>(await(await s.performPost("local_lsf_unification","dashboard/import",{body:e})).json()).status,m=async e=>(await(await s.performPost("local_lsf_unification","dashboard/requestaction",{body:e})).json()).status,d=async()=>await(await s.performGet("local_lsf_unification","dashboard/teachers")).json(),l=async()=>await(await s.performGet("local_lsf_unification","dashboard/categories")).json();export{l as fetchCategories,a as fetchDashboardCourses,n as fetchOwnCourses,c as fetchRequests,i as fetchTeacherCourses,d as fetchTeachers,p as submitCourseImport,u as submitCourseRequest,m as submitRequestAction}; 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": "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", + "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