iobroker.webuntis2 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.de.md +79 -0
- package/README.md +74 -0
- package/admin/build/index.css +2 -0
- package/admin/build/index.css.map +7 -0
- package/admin/build/index.js +171 -0
- package/admin/build/index.js.map +7 -0
- package/admin/index.html +24 -0
- package/admin/index_m.html +24 -0
- package/admin/style.css +32 -0
- package/admin/webuntis2.png +0 -0
- package/build/lib/LegacyNamespaceCleanup.js +33 -0
- package/build/lib/LegacyNamespaceCleanup.js.map +7 -0
- package/build/lib/webuntis/CurrentNextLesson.js +98 -0
- package/build/lib/webuntis/CurrentNextLesson.js.map +7 -0
- package/build/lib/webuntis/Holidays.js +138 -0
- package/build/lib/webuntis/Holidays.js.map +7 -0
- package/build/lib/webuntis/SchoolDiscovery.js +145 -0
- package/build/lib/webuntis/SchoolDiscovery.js.map +7 -0
- package/build/lib/webuntis/Timetable.js +210 -0
- package/build/lib/webuntis/Timetable.js.map +7 -0
- package/build/lib/webuntis/WebUntisClient.js +593 -0
- package/build/lib/webuntis/WebUntisClient.js.map +7 -0
- package/build/lib/webuntis/WebUntisErrors.js +53 -0
- package/build/lib/webuntis/WebUntisErrors.js.map +7 -0
- package/build/lib/webuntis/WebUntisService.js +252 -0
- package/build/lib/webuntis/WebUntisService.js.map +7 -0
- package/build/lib/webuntis/WebUntisTypes.js +17 -0
- package/build/lib/webuntis/WebUntisTypes.js.map +7 -0
- package/build/main.js +514 -0
- package/build/main.js.map +7 -0
- package/io-package.json +151 -0
- package/package.json +111 -0
package/admin/index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<!-- Load ioBroker scripts and styles-->
|
|
6
|
+
<link rel="stylesheet" type="text/css" href="../../css/adapter.css" />
|
|
7
|
+
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
|
8
|
+
|
|
9
|
+
<!-- Load our own files -->
|
|
10
|
+
<link rel="stylesheet" type="text/css" href="style.css" />
|
|
11
|
+
|
|
12
|
+
</head>
|
|
13
|
+
|
|
14
|
+
<body>
|
|
15
|
+
|
|
16
|
+
<!-- this is where the React components are loaded into -->
|
|
17
|
+
<div class="m adapter-container" id="root"></div>
|
|
18
|
+
|
|
19
|
+
<!-- load compiled React scripts -->
|
|
20
|
+
<script type="text/javascript" src="build/index.js"></script>
|
|
21
|
+
|
|
22
|
+
</body>
|
|
23
|
+
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<!-- Load ioBroker scripts and styles-->
|
|
6
|
+
<link rel="stylesheet" type="text/css" href="../../css/adapter.css" />
|
|
7
|
+
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
|
8
|
+
|
|
9
|
+
<!-- Load our own files -->
|
|
10
|
+
<link rel="stylesheet" type="text/css" href="style.css" />
|
|
11
|
+
|
|
12
|
+
</head>
|
|
13
|
+
|
|
14
|
+
<body>
|
|
15
|
+
|
|
16
|
+
<!-- this is where the React components are loaded into -->
|
|
17
|
+
<div class="m adapter-container" id="root"></div>
|
|
18
|
+
|
|
19
|
+
<!-- load compiled React scripts -->
|
|
20
|
+
<script type="text/javascript" src="build/index.js"></script>
|
|
21
|
+
|
|
22
|
+
</body>
|
|
23
|
+
|
|
24
|
+
</html>
|
package/admin/style.css
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* You can delete those if you want. I just found them very helpful */
|
|
2
|
+
* {
|
|
3
|
+
box-sizing: border-box
|
|
4
|
+
}
|
|
5
|
+
.m {
|
|
6
|
+
/* Don't cut off dropdowns! */
|
|
7
|
+
overflow: initial;
|
|
8
|
+
}
|
|
9
|
+
.m.adapter-container,
|
|
10
|
+
.m.adapter-container > div.App {
|
|
11
|
+
/* Fix layout/scrolling issues with tabs */
|
|
12
|
+
height: 100%;
|
|
13
|
+
width: 100%;
|
|
14
|
+
position: relative;
|
|
15
|
+
}
|
|
16
|
+
.m .select-wrapper + label {
|
|
17
|
+
/* The positioning for dropdown labels is messed up */
|
|
18
|
+
transform: none !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
label > i[title] {
|
|
22
|
+
/* Display the help cursor for the tooltip icons and fix their positioning */
|
|
23
|
+
cursor: help;
|
|
24
|
+
margin-left: 0.25em;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.dropdown-content {
|
|
28
|
+
/* Don't wrap text in dropdowns */
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Add your styles here */
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var LegacyNamespaceCleanup_exports = {};
|
|
20
|
+
__export(LegacyNamespaceCleanup_exports, {
|
|
21
|
+
cleanupLegacyNamespace: () => cleanupLegacyNamespace
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(LegacyNamespaceCleanup_exports);
|
|
24
|
+
async function cleanupLegacyNamespace(adapter, namespace) {
|
|
25
|
+
if (await adapter.getObjectAsync(namespace)) {
|
|
26
|
+
await adapter.delObjectAsync(namespace, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
30
|
+
0 && (module.exports = {
|
|
31
|
+
cleanupLegacyNamespace
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=LegacyNamespaceCleanup.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/LegacyNamespaceCleanup.ts"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable jsdoc/require-jsdoc */\n\nexport interface NamespaceCleanupAdapter {\n\tgetObjectAsync(id: string): Promise<unknown>;\n\tdelObjectAsync(id: string, options: { recursive: boolean }): Promise<void>;\n}\n\nexport async function cleanupLegacyNamespace(adapter: NamespaceCleanupAdapter, namespace: string): Promise<void> {\n\tif (await adapter.getObjectAsync(namespace)) {\n\t\tawait adapter.delObjectAsync(namespace, { recursive: true });\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,eAAsB,uBAAuB,SAAkC,WAAkC;AAChH,MAAI,MAAM,QAAQ,eAAe,SAAS,GAAG;AAC5C,UAAM,QAAQ,eAAe,WAAW,EAAE,WAAW,KAAK,CAAC;AAAA,EAC5D;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var CurrentNextLesson_exports = {};
|
|
20
|
+
__export(CurrentNextLesson_exports, {
|
|
21
|
+
deriveCurrentNextLesson: () => deriveCurrentNextLesson
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(CurrentNextLesson_exports);
|
|
24
|
+
var import_Timetable = require("./Timetable");
|
|
25
|
+
const EMPTY_SUMMARY = {
|
|
26
|
+
currentLesson: "",
|
|
27
|
+
currentSubject: "",
|
|
28
|
+
currentRoom: "",
|
|
29
|
+
currentTeacher: "",
|
|
30
|
+
nextLesson: "",
|
|
31
|
+
nextSubject: "",
|
|
32
|
+
nextRoom: "",
|
|
33
|
+
nextTeacher: "",
|
|
34
|
+
nextLessonStart: "",
|
|
35
|
+
minutesUntilNextLesson: 0,
|
|
36
|
+
schoolRunning: false,
|
|
37
|
+
minutesUntilSchoolEnd: 0
|
|
38
|
+
};
|
|
39
|
+
function localDate(date) {
|
|
40
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
41
|
+
}
|
|
42
|
+
function lessonTime(date, time) {
|
|
43
|
+
const match = /^(\d{2}):(\d{2})$/.exec(time);
|
|
44
|
+
if (!match) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const result = new Date(date);
|
|
48
|
+
result.setHours(Number(match[1]), Number(match[2]), 0, 0);
|
|
49
|
+
return Number.isNaN(result.getTime()) ? null : result.getTime();
|
|
50
|
+
}
|
|
51
|
+
function details(lesson) {
|
|
52
|
+
return {
|
|
53
|
+
currentLesson: (0, import_Timetable.lessonChannelName)(lesson),
|
|
54
|
+
currentSubject: lesson.subjectLong || lesson.subject || "",
|
|
55
|
+
currentRoom: lesson.room || "",
|
|
56
|
+
currentTeacher: lesson.teacher || ""
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function nextDetails(lesson) {
|
|
60
|
+
return {
|
|
61
|
+
nextLesson: (0, import_Timetable.lessonChannelName)(lesson),
|
|
62
|
+
nextSubject: lesson.subjectLong || lesson.subject || "",
|
|
63
|
+
nextRoom: lesson.room || "",
|
|
64
|
+
nextTeacher: lesson.teacher || "",
|
|
65
|
+
nextLessonStart: lesson.startTime || ""
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function deriveCurrentNextLesson(lessons, now) {
|
|
69
|
+
const nowMs = now.getTime();
|
|
70
|
+
const activeLessons = lessons.filter((lesson) => lesson.date === localDate(now) && !lesson.cancelled).flatMap((lesson) => {
|
|
71
|
+
const start = lessonTime(now, lesson.startTime);
|
|
72
|
+
const end = lessonTime(now, lesson.endTime);
|
|
73
|
+
return start === null || end === null ? [] : [{ lesson, start, end }];
|
|
74
|
+
}).sort((a, b) => a.start - b.start || a.end - b.end || a.lesson.id.localeCompare(b.lesson.id));
|
|
75
|
+
if (!activeLessons.length) {
|
|
76
|
+
return { ...EMPTY_SUMMARY };
|
|
77
|
+
}
|
|
78
|
+
const current = activeLessons.find((item) => item.start <= nowMs && nowMs < item.end);
|
|
79
|
+
const next = activeLessons.find((item) => item.start > nowMs);
|
|
80
|
+
const first = activeLessons[0];
|
|
81
|
+
const last = activeLessons.at(-1);
|
|
82
|
+
const schoolRunning = nowMs >= first.start && nowMs < last.end;
|
|
83
|
+
return {
|
|
84
|
+
...EMPTY_SUMMARY,
|
|
85
|
+
...current ? details(current.lesson) : {},
|
|
86
|
+
...next ? {
|
|
87
|
+
...nextDetails(next.lesson),
|
|
88
|
+
minutesUntilNextLesson: Math.ceil((next.start - nowMs) / 6e4)
|
|
89
|
+
} : {},
|
|
90
|
+
schoolRunning,
|
|
91
|
+
minutesUntilSchoolEnd: schoolRunning ? Math.ceil((last.end - nowMs) / 6e4) : 0
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
95
|
+
0 && (module.exports = {
|
|
96
|
+
deriveCurrentNextLesson
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=CurrentNextLesson.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/webuntis/CurrentNextLesson.ts"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable jsdoc/require-jsdoc */\n\nimport { lessonChannelName, type TimetableLesson } from \"./Timetable\";\n\nexport interface CurrentNextLessonSummary {\n\tcurrentLesson: string;\n\tcurrentSubject: string;\n\tcurrentRoom: string;\n\tcurrentTeacher: string;\n\tnextLesson: string;\n\tnextSubject: string;\n\tnextRoom: string;\n\tnextTeacher: string;\n\tnextLessonStart: string;\n\tminutesUntilNextLesson: number;\n\tschoolRunning: boolean;\n\tminutesUntilSchoolEnd: number;\n}\n\nconst EMPTY_SUMMARY: CurrentNextLessonSummary = {\n\tcurrentLesson: \"\",\n\tcurrentSubject: \"\",\n\tcurrentRoom: \"\",\n\tcurrentTeacher: \"\",\n\tnextLesson: \"\",\n\tnextSubject: \"\",\n\tnextRoom: \"\",\n\tnextTeacher: \"\",\n\tnextLessonStart: \"\",\n\tminutesUntilNextLesson: 0,\n\tschoolRunning: false,\n\tminutesUntilSchoolEnd: 0,\n};\n\ninterface TimedLesson {\n\tlesson: TimetableLesson;\n\tstart: number;\n\tend: number;\n}\n\nfunction localDate(date: Date): string {\n\treturn `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n}\n\nfunction lessonTime(date: Date, time: string): number | null {\n\tconst match = /^(\\d{2}):(\\d{2})$/.exec(time);\n\tif (!match) {\n\t\treturn null;\n\t}\n\tconst result = new Date(date);\n\tresult.setHours(Number(match[1]), Number(match[2]), 0, 0);\n\treturn Number.isNaN(result.getTime()) ? null : result.getTime();\n}\n\nfunction details(\n\tlesson: TimetableLesson,\n): Pick<CurrentNextLessonSummary, \"currentLesson\" | \"currentSubject\" | \"currentRoom\" | \"currentTeacher\"> {\n\treturn {\n\t\tcurrentLesson: lessonChannelName(lesson),\n\t\tcurrentSubject: lesson.subjectLong || lesson.subject || \"\",\n\t\tcurrentRoom: lesson.room || \"\",\n\t\tcurrentTeacher: lesson.teacher || \"\",\n\t};\n}\n\nfunction nextDetails(\n\tlesson: TimetableLesson,\n): Pick<CurrentNextLessonSummary, \"nextLesson\" | \"nextSubject\" | \"nextRoom\" | \"nextTeacher\" | \"nextLessonStart\"> {\n\treturn {\n\t\tnextLesson: lessonChannelName(lesson),\n\t\tnextSubject: lesson.subjectLong || lesson.subject || \"\",\n\t\tnextRoom: lesson.room || \"\",\n\t\tnextTeacher: lesson.teacher || \"\",\n\t\tnextLessonStart: lesson.startTime || \"\",\n\t};\n}\n\nexport function deriveCurrentNextLesson(lessons: TimetableLesson[], now: Date): CurrentNextLessonSummary {\n\tconst nowMs = now.getTime();\n\tconst activeLessons: TimedLesson[] = lessons\n\t\t.filter(lesson => lesson.date === localDate(now) && !lesson.cancelled)\n\t\t.flatMap(lesson => {\n\t\t\tconst start = lessonTime(now, lesson.startTime);\n\t\t\tconst end = lessonTime(now, lesson.endTime);\n\t\t\treturn start === null || end === null ? [] : [{ lesson, start, end }];\n\t\t})\n\t\t.sort((a, b) => a.start - b.start || a.end - b.end || a.lesson.id.localeCompare(b.lesson.id));\n\n\tif (!activeLessons.length) {\n\t\treturn { ...EMPTY_SUMMARY };\n\t}\n\n\tconst current = activeLessons.find(item => item.start <= nowMs && nowMs < item.end);\n\tconst next = activeLessons.find(item => item.start > nowMs);\n\tconst first = activeLessons[0];\n\tconst last = activeLessons.at(-1)!;\n\tconst schoolRunning = nowMs >= first.start && nowMs < last.end;\n\n\treturn {\n\t\t...EMPTY_SUMMARY,\n\t\t...(current ? details(current.lesson) : {}),\n\t\t...(next\n\t\t\t? {\n\t\t\t\t\t...nextDetails(next.lesson),\n\t\t\t\t\tminutesUntilNextLesson: Math.ceil((next.start - nowMs) / 60000),\n\t\t\t\t}\n\t\t\t: {}),\n\t\tschoolRunning,\n\t\tminutesUntilSchoolEnd: schoolRunning ? Math.ceil((last.end - nowMs) / 60000) : 0,\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var Holidays_exports = {};
|
|
20
|
+
__export(Holidays_exports, {
|
|
21
|
+
HolidayCache: () => HolidayCache,
|
|
22
|
+
normalizeHolidayEntries: () => normalizeHolidayEntries,
|
|
23
|
+
summarizeHolidays: () => summarizeHolidays
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(Holidays_exports);
|
|
26
|
+
const EMPTY_DAY = { name: "", longName: "", startDate: "", endDate: "" };
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === "object" && value !== null;
|
|
29
|
+
}
|
|
30
|
+
function datePart(value) {
|
|
31
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
32
|
+
value = String(value);
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== "string") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const digits = value.replace(/-/g, "");
|
|
38
|
+
if (!/^\d{8}$/.test(digits)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const year = Number(digits.slice(0, 4));
|
|
42
|
+
const month = Number(digits.slice(4, 6));
|
|
43
|
+
const day = Number(digits.slice(6, 8));
|
|
44
|
+
const check = new Date(Date.UTC(year, month - 1, day));
|
|
45
|
+
if (check.getUTCFullYear() !== year || check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
|
|
49
|
+
}
|
|
50
|
+
function text(value) {
|
|
51
|
+
return typeof value === "string" ? value.trim() : "";
|
|
52
|
+
}
|
|
53
|
+
function localDatePart(date) {
|
|
54
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
55
|
+
}
|
|
56
|
+
function daySerial(value) {
|
|
57
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
58
|
+
return Math.floor(Date.UTC(year, month - 1, day) / 864e5);
|
|
59
|
+
}
|
|
60
|
+
function emptySummary() {
|
|
61
|
+
return {
|
|
62
|
+
current: { ...EMPTY_DAY, active: false },
|
|
63
|
+
next: { ...EMPTY_DAY, daysUntil: 0 }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeHolidayEntries(value) {
|
|
67
|
+
if (!Array.isArray(value)) {
|
|
68
|
+
throw new Error("Invalid WebUntis holiday result");
|
|
69
|
+
}
|
|
70
|
+
return value.flatMap((item) => {
|
|
71
|
+
if (!isRecord(item)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const startDate = datePart(item.startDate);
|
|
75
|
+
const endDate = datePart(item.endDate);
|
|
76
|
+
if (!startDate || !endDate || startDate > endDate) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
id: typeof item.id === "number" || typeof item.id === "string" ? item.id : null,
|
|
82
|
+
name: text(item.name),
|
|
83
|
+
longName: text(item.longName),
|
|
84
|
+
startDate,
|
|
85
|
+
endDate
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function summarizeHolidays(entries, now) {
|
|
91
|
+
const today = localDatePart(now);
|
|
92
|
+
const sorted = [...entries].sort((left, right) => left.startDate.localeCompare(right.startDate));
|
|
93
|
+
const current = sorted.find((entry) => entry.startDate <= today && entry.endDate >= today);
|
|
94
|
+
const next = sorted.find((entry) => entry.startDate > today);
|
|
95
|
+
const result = emptySummary();
|
|
96
|
+
if (current) {
|
|
97
|
+
result.current = {
|
|
98
|
+
name: current.name,
|
|
99
|
+
longName: current.longName || current.name,
|
|
100
|
+
startDate: current.startDate,
|
|
101
|
+
endDate: current.endDate,
|
|
102
|
+
active: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (next) {
|
|
106
|
+
result.next = {
|
|
107
|
+
name: next.name,
|
|
108
|
+
longName: next.longName || next.name,
|
|
109
|
+
startDate: next.startDate,
|
|
110
|
+
endDate: next.endDate,
|
|
111
|
+
daysUntil: daySerial(next.startDate) - daySerial(today)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
class HolidayCache {
|
|
117
|
+
constructor(ttlMs = 6 * 60 * 60 * 1e3) {
|
|
118
|
+
this.ttlMs = ttlMs;
|
|
119
|
+
}
|
|
120
|
+
entries = null;
|
|
121
|
+
fetchedAt = 0;
|
|
122
|
+
async get(now, loader) {
|
|
123
|
+
if (this.entries && now.getTime() - this.fetchedAt < this.ttlMs) {
|
|
124
|
+
return this.entries;
|
|
125
|
+
}
|
|
126
|
+
const entries = normalizeHolidayEntries(await loader());
|
|
127
|
+
this.entries = entries;
|
|
128
|
+
this.fetchedAt = now.getTime();
|
|
129
|
+
return entries;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
133
|
+
0 && (module.exports = {
|
|
134
|
+
HolidayCache,
|
|
135
|
+
normalizeHolidayEntries,
|
|
136
|
+
summarizeHolidays
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=Holidays.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/webuntis/Holidays.ts"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable jsdoc/require-jsdoc */\n\nexport interface HolidayEntry {\n\tid: number | string | null;\n\tname: string;\n\tlongName: string;\n\tstartDate: string;\n\tendDate: string;\n}\n\nexport interface HolidayDaySummary {\n\tname: string;\n\tlongName: string;\n\tstartDate: string;\n\tendDate: string;\n}\n\nexport interface HolidaySummary {\n\tcurrent: HolidayDaySummary & { active: boolean };\n\tnext: HolidayDaySummary & { daysUntil: number };\n}\n\nconst EMPTY_DAY = { name: \"\", longName: \"\", startDate: \"\", endDate: \"\" };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction datePart(value: unknown): string | null {\n\tif (typeof value === \"number\" && Number.isInteger(value)) {\n\t\tvalue = String(value);\n\t}\n\tif (typeof value !== \"string\") {\n\t\treturn null;\n\t}\n\tconst digits = value.replace(/-/g, \"\");\n\tif (!/^\\d{8}$/.test(digits)) {\n\t\treturn null;\n\t}\n\tconst year = Number(digits.slice(0, 4));\n\tconst month = Number(digits.slice(4, 6));\n\tconst day = Number(digits.slice(6, 8));\n\tconst check = new Date(Date.UTC(year, month - 1, day));\n\tif (check.getUTCFullYear() !== year || check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day) {\n\t\treturn null;\n\t}\n\treturn `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;\n}\n\nfunction text(value: unknown): string {\n\treturn typeof value === \"string\" ? value.trim() : \"\";\n}\n\nfunction localDatePart(date: Date): string {\n\treturn `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n}\n\nfunction daySerial(value: string): number {\n\tconst [year, month, day] = value.split(\"-\").map(Number);\n\treturn Math.floor(Date.UTC(year, month - 1, day) / 86_400_000);\n}\n\nfunction emptySummary(): HolidaySummary {\n\treturn {\n\t\tcurrent: { ...EMPTY_DAY, active: false },\n\t\tnext: { ...EMPTY_DAY, daysUntil: 0 },\n\t};\n}\n\nexport function normalizeHolidayEntries(value: unknown): HolidayEntry[] {\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(\"Invalid WebUntis holiday result\");\n\t}\n\treturn value.flatMap(item => {\n\t\tif (!isRecord(item)) {\n\t\t\treturn [];\n\t\t}\n\t\tconst startDate = datePart(item.startDate);\n\t\tconst endDate = datePart(item.endDate);\n\t\tif (!startDate || !endDate || startDate > endDate) {\n\t\t\treturn [];\n\t\t}\n\t\treturn [\n\t\t\t{\n\t\t\t\tid: typeof item.id === \"number\" || typeof item.id === \"string\" ? item.id : null,\n\t\t\t\tname: text(item.name),\n\t\t\t\tlongName: text(item.longName),\n\t\t\t\tstartDate,\n\t\t\t\tendDate,\n\t\t\t},\n\t\t];\n\t});\n}\n\nexport function summarizeHolidays(entries: HolidayEntry[], now: Date): HolidaySummary {\n\tconst today = localDatePart(now);\n\tconst sorted = [...entries].sort((left, right) => left.startDate.localeCompare(right.startDate));\n\tconst current = sorted.find(entry => entry.startDate <= today && entry.endDate >= today);\n\tconst next = sorted.find(entry => entry.startDate > today);\n\tconst result = emptySummary();\n\tif (current) {\n\t\tresult.current = {\n\t\t\tname: current.name,\n\t\t\tlongName: current.longName || current.name,\n\t\t\tstartDate: current.startDate,\n\t\t\tendDate: current.endDate,\n\t\t\tactive: true,\n\t\t};\n\t}\n\tif (next) {\n\t\tresult.next = {\n\t\t\tname: next.name,\n\t\t\tlongName: next.longName || next.name,\n\t\t\tstartDate: next.startDate,\n\t\t\tendDate: next.endDate,\n\t\t\tdaysUntil: daySerial(next.startDate) - daySerial(today),\n\t\t};\n\t}\n\treturn result;\n}\n\nexport class HolidayCache {\n\tprivate entries: HolidayEntry[] | null = null;\n\tprivate fetchedAt = 0;\n\n\tpublic constructor(private readonly ttlMs = 6 * 60 * 60 * 1000) {}\n\n\tpublic async get(now: Date, loader: () => Promise<unknown>): Promise<HolidayEntry[] | null> {\n\t\tif (this.entries && now.getTime() - this.fetchedAt < this.ttlMs) {\n\t\t\treturn this.entries;\n\t\t}\n\t\tconst entries = normalizeHolidayEntries(await loader());\n\t\tthis.entries = entries;\n\t\tthis.fetchedAt = now.getTime();\n\t\treturn entries;\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var SchoolDiscovery_exports = {};
|
|
20
|
+
__export(SchoolDiscovery_exports, {
|
|
21
|
+
mergeSchoolSelection: () => mergeSchoolSelection,
|
|
22
|
+
rankSchoolResults: () => rankSchoolResults,
|
|
23
|
+
schoolResultToConfig: () => schoolResultToConfig,
|
|
24
|
+
searchSchools: () => searchSchools
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(SchoolDiscovery_exports);
|
|
27
|
+
var import_WebUntisErrors = require("./WebUntisErrors");
|
|
28
|
+
const SEARCH_URL = "https://mobile.webuntis.com/ms/schoolquery2";
|
|
29
|
+
const REQUEST_TIMEOUT_MS = 1e4;
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === "object" && value !== null;
|
|
32
|
+
}
|
|
33
|
+
function asString(value, field) {
|
|
34
|
+
if (typeof value !== "string") {
|
|
35
|
+
throw new import_WebUntisErrors.WebUntisError("INVALID_RESPONSE", `Missing school field: ${field}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function mapSchool(value) {
|
|
40
|
+
if (!isRecord(value)) {
|
|
41
|
+
throw new import_WebUntisErrors.WebUntisError("INVALID_RESPONSE", "Invalid school search result");
|
|
42
|
+
}
|
|
43
|
+
if (typeof value.schoolId !== "number") {
|
|
44
|
+
throw new import_WebUntisErrors.WebUntisError("INVALID_RESPONSE", "Missing school field: schoolId");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
displayName: asString(value.displayName, "displayName"),
|
|
48
|
+
address: asString(value.address, "address"),
|
|
49
|
+
loginName: asString(value.loginName, "loginName"),
|
|
50
|
+
schoolId: value.schoolId,
|
|
51
|
+
server: asString(value.server, "server"),
|
|
52
|
+
serverUrl: asString(value.serverUrl, "serverUrl"),
|
|
53
|
+
mobileServiceUrl: value.mobileServiceUrl === null ? null : asString(value.mobileServiceUrl, "mobileServiceUrl")
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isSchoolSearchResponse(value) {
|
|
57
|
+
return isRecord(value) && typeof value.size === "number" && Array.isArray(value.schools);
|
|
58
|
+
}
|
|
59
|
+
async function searchSchools(query, fetchImpl = fetch) {
|
|
60
|
+
const normalizedQuery = query.trim();
|
|
61
|
+
if (normalizedQuery.length < 2) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetchImpl(SEARCH_URL, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
id: Date.now().toString(),
|
|
72
|
+
method: "searchSchool",
|
|
73
|
+
params: [{ schoolid: 0, search: normalizedQuery }],
|
|
74
|
+
jsonrpc: "2.0"
|
|
75
|
+
}),
|
|
76
|
+
signal: controller.signal
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new import_WebUntisErrors.WebUntisError("SERVER_UNREACHABLE", `School search HTTP ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
const payload = await response.json();
|
|
82
|
+
if (isRecord(payload) && isRecord(payload.error) && payload.error.code === -6003) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(payload) || !isSchoolSearchResponse(payload.result)) {
|
|
86
|
+
throw new import_WebUntisErrors.WebUntisError("INVALID_RESPONSE", "Invalid school search response");
|
|
87
|
+
}
|
|
88
|
+
return payload.result.schools.map(mapSchool);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof import_WebUntisErrors.WebUntisError) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
94
|
+
throw new import_WebUntisErrors.WebUntisError("TIMEOUT", "School search timed out");
|
|
95
|
+
}
|
|
96
|
+
throw new import_WebUntisErrors.WebUntisError("SERVER_UNREACHABLE", "School search failed", { cause: error });
|
|
97
|
+
} finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function rankSchoolResults(results, query) {
|
|
102
|
+
const tokens = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
103
|
+
const collator = new Intl.Collator("de", { sensitivity: "base" });
|
|
104
|
+
const score = (school) => {
|
|
105
|
+
const fields = [school.displayName, school.address, school.loginName, school.server].map(
|
|
106
|
+
(value) => value.toLocaleLowerCase()
|
|
107
|
+
);
|
|
108
|
+
return tokens.reduce((total, token) => {
|
|
109
|
+
if (fields.some((field) => field === token)) {
|
|
110
|
+
return total + 100;
|
|
111
|
+
}
|
|
112
|
+
if (fields.some((field) => field.startsWith(token))) {
|
|
113
|
+
return total + 20;
|
|
114
|
+
}
|
|
115
|
+
if (fields.some((field) => field.includes(token))) {
|
|
116
|
+
return total + 10;
|
|
117
|
+
}
|
|
118
|
+
return total;
|
|
119
|
+
}, 0);
|
|
120
|
+
};
|
|
121
|
+
return [...results].sort((left, right) => {
|
|
122
|
+
const scoreDifference = score(right) - score(left);
|
|
123
|
+
return scoreDifference || collator.compare(left.displayName, right.displayName);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function schoolResultToConfig(school) {
|
|
127
|
+
return {
|
|
128
|
+
server: school.serverUrl,
|
|
129
|
+
schoolName: school.loginName,
|
|
130
|
+
schoolDisplayName: school.displayName,
|
|
131
|
+
schoolAddress: school.address,
|
|
132
|
+
schoolId: school.schoolId
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function mergeSchoolSelection(native, school) {
|
|
136
|
+
return { ...native, ...schoolResultToConfig(school) };
|
|
137
|
+
}
|
|
138
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
139
|
+
0 && (module.exports = {
|
|
140
|
+
mergeSchoolSelection,
|
|
141
|
+
rankSchoolResults,
|
|
142
|
+
schoolResultToConfig,
|
|
143
|
+
searchSchools
|
|
144
|
+
});
|
|
145
|
+
//# sourceMappingURL=SchoolDiscovery.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/webuntis/SchoolDiscovery.ts"],
|
|
4
|
+
"sourcesContent": ["/* eslint-disable jsdoc/no-blank-blocks, jsdoc/require-param */\n\nimport { WebUntisError } from \"./WebUntisErrors\";\nimport type { SchoolSearchResponse, SchoolSearchResult, WebUntisSchoolConfig } from \"./WebUntisTypes\";\n\nconst SEARCH_URL = \"https://mobile.webuntis.com/ms/schoolquery2\";\nconst REQUEST_TIMEOUT_MS = 10_000;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction asString(value: unknown, field: string): string {\n\tif (typeof value !== \"string\") {\n\t\tthrow new WebUntisError(\"INVALID_RESPONSE\", `Missing school field: ${field}`);\n\t}\n\treturn value;\n}\n\nfunction mapSchool(value: unknown): SchoolSearchResult {\n\tif (!isRecord(value)) {\n\t\tthrow new WebUntisError(\"INVALID_RESPONSE\", \"Invalid school search result\");\n\t}\n\tif (typeof value.schoolId !== \"number\") {\n\t\tthrow new WebUntisError(\"INVALID_RESPONSE\", \"Missing school field: schoolId\");\n\t}\n\treturn {\n\t\tdisplayName: asString(value.displayName, \"displayName\"),\n\t\taddress: asString(value.address, \"address\"),\n\t\tloginName: asString(value.loginName, \"loginName\"),\n\t\tschoolId: value.schoolId,\n\t\tserver: asString(value.server, \"server\"),\n\t\tserverUrl: asString(value.serverUrl, \"serverUrl\"),\n\t\tmobileServiceUrl: value.mobileServiceUrl === null ? null : asString(value.mobileServiceUrl, \"mobileServiceUrl\"),\n\t};\n}\n\nfunction isSchoolSearchResponse(value: unknown): value is SchoolSearchResponse {\n\treturn isRecord(value) && typeof value.size === \"number\" && Array.isArray(value.schools);\n}\n\n/**\n *\n */\nexport async function searchSchools(query: string, fetchImpl: typeof fetch = fetch): Promise<SchoolSearchResult[]> {\n\tconst normalizedQuery = query.trim();\n\tif (normalizedQuery.length < 2) {\n\t\treturn [];\n\t}\n\tconst controller = new AbortController();\n\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\ttry {\n\t\tconst response = await fetchImpl(SEARCH_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json\" },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tid: Date.now().toString(),\n\t\t\t\tmethod: \"searchSchool\",\n\t\t\t\tparams: [{ schoolid: 0, search: normalizedQuery }],\n\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t}),\n\t\t\tsignal: controller.signal,\n\t\t});\n\t\tif (!response.ok) {\n\t\t\tthrow new WebUntisError(\"SERVER_UNREACHABLE\", `School search HTTP ${response.status}`);\n\t\t}\n\t\tconst payload: unknown = await response.json();\n\t\tif (isRecord(payload) && isRecord(payload.error) && payload.error.code === -6003) {\n\t\t\treturn [];\n\t\t}\n\t\tif (!isRecord(payload) || !isSchoolSearchResponse(payload.result)) {\n\t\t\tthrow new WebUntisError(\"INVALID_RESPONSE\", \"Invalid school search response\");\n\t\t}\n\t\treturn payload.result.schools.map(mapSchool);\n\t} catch (error) {\n\t\tif (error instanceof WebUntisError) {\n\t\t\tthrow error;\n\t\t}\n\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\n\t\t\tthrow new WebUntisError(\"TIMEOUT\", \"School search timed out\");\n\t\t}\n\t\tthrow new WebUntisError(\"SERVER_UNREACHABLE\", \"School search failed\", { cause: error });\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/**\n * Sorts search results by their match with the entered free-text query.\n */\nexport function rankSchoolResults(results: SchoolSearchResult[], query: string): SchoolSearchResult[] {\n\tconst tokens = query.trim().toLocaleLowerCase().split(/\\s+/).filter(Boolean);\n\tconst collator = new Intl.Collator(\"de\", { sensitivity: \"base\" });\n\n\tconst score = (school: SchoolSearchResult): number => {\n\t\tconst fields = [school.displayName, school.address, school.loginName, school.server].map(value =>\n\t\t\tvalue.toLocaleLowerCase(),\n\t\t);\n\t\treturn tokens.reduce((total, token) => {\n\t\t\tif (fields.some(field => field === token)) {\n\t\t\t\treturn total + 100;\n\t\t\t}\n\t\t\tif (fields.some(field => field.startsWith(token))) {\n\t\t\t\treturn total + 20;\n\t\t\t}\n\t\t\tif (fields.some(field => field.includes(token))) {\n\t\t\t\treturn total + 10;\n\t\t\t}\n\t\t\treturn total;\n\t\t}, 0);\n\t};\n\n\treturn [...results].sort((left, right) => {\n\t\tconst scoreDifference = score(right) - score(left);\n\t\treturn scoreDifference || collator.compare(left.displayName, right.displayName);\n\t});\n}\n\n/**\n * Converts one search result into the native school configuration fields.\n */\nexport function schoolResultToConfig(school: SchoolSearchResult): WebUntisSchoolConfig {\n\treturn {\n\t\tserver: school.serverUrl,\n\t\tschoolName: school.loginName,\n\t\tschoolDisplayName: school.displayName,\n\t\tschoolAddress: school.address,\n\t\tschoolId: school.schoolId,\n\t};\n}\n\n/**\n * Replaces the selected school atomically while preserving credentials and other native values.\n */\nexport function mergeSchoolSelection(\n\tnative: Record<string, unknown>,\n\tschool: SchoolSearchResult,\n): Record<string, unknown> {\n\treturn { ...native, ...schoolResultToConfig(school) };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|