chuvsu-js 2.4.2 → 2.6.0

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/dist/browser.d.ts CHANGED
@@ -2,4 +2,4 @@ export { Schedule } from "./tt/schedule.js";
2
2
  export { getCurrentPeriod, isSessionPeriod, getSemesterStart, getSemesterWeeks, getWeekNumber, getWeekdayName, getTimeSlots, getLessonNumber, getAdjacentSemester, } from "./tt/utils.js";
3
3
  export { Period, EducationType } from "./common/types.js";
4
4
  export type { Time, WeekRange, Teacher } from "./common/types.js";
5
- export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
5
+ export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
package/dist/index.d.ts CHANGED
@@ -7,4 +7,4 @@ export type { Holiday, HolidayTransfer } from "./tt/utils.js";
7
7
  export { Period, EducationType, AuthError, ParseError, } from "./common/types.js";
8
8
  export type { Time, WeekRange, Teacher } from "./common/types.js";
9
9
  export type { PersonalData } from "./lk/types.js";
10
- export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, TransferInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
10
+ export type { Faculty, Group, ScheduleEntry, FullScheduleSlot, FullScheduleDay, LessonTimeSlot, Lesson, LessonTime, SemesterWeek, Substitution, SubstituteForInfo, TransferInfo, TeacherInfo, TtClientOptions, CacheConfig, } from "./tt/types.js";
@@ -1,7 +1,7 @@
1
1
  import type { CacheEntry } from "../common/cache.js";
2
2
  import { Period } from "../common/types.js";
3
3
  import { Schedule } from "./schedule.js";
4
- import type { Faculty, Group, TtClientOptions, CacheConfig } from "./types.js";
4
+ import type { Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig } from "./types.js";
5
5
  export declare class TtClient {
6
6
  private http;
7
7
  private educationType;
@@ -47,4 +47,15 @@ export declare class TtClient {
47
47
  id: number;
48
48
  name: string;
49
49
  }[]>;
50
+ getTeachers(): Promise<{
51
+ id: number;
52
+ name: string;
53
+ }[]>;
54
+ private fetchTeacherSchedule;
55
+ getTeacherSchedule(teacherId: number): Promise<Schedule>;
56
+ getTeacherScheduleForPeriod(opts: {
57
+ teacherId: number;
58
+ period: Period;
59
+ }): Promise<Schedule>;
60
+ getTeacherInfo(teacherId: number): Promise<TeacherInfo | null>;
50
61
  }
package/dist/tt/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { HttpClient } from "../common/http.js";
2
2
  import { Cache } from "../common/cache.js";
3
3
  import { AuthError } from "../common/types.js";
4
- import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, } from "./parse.js";
4
+ import { parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse.js";
5
5
  import { Schedule } from "./schedule.js";
6
6
  const BASE = "https://tt.chuvsu.ru";
7
7
  const AUTH_URL = `${BASE}/auth`;
@@ -173,4 +173,47 @@ export class TtClient {
173
173
  });
174
174
  return parseTeacherButtons(body);
175
175
  }
176
+ // --- Teacher schedule ---
177
+ async getTeachers() {
178
+ const cached = this.cache?.get("teachers", "all");
179
+ if (cached)
180
+ return cached;
181
+ const { body } = await this.authGet(`${BASE}/index/tech`);
182
+ const data = parseTeacherButtons(body);
183
+ this.cache?.set("teachers", "all", data);
184
+ return data;
185
+ }
186
+ async fetchTeacherSchedule(teacherId, period) {
187
+ const cacheKey = `teacher:${teacherId}:${period}`;
188
+ const cached = this.cache?.get("schedule", cacheKey);
189
+ if (cached)
190
+ return cached;
191
+ const url = `${BASE}/index/techtt/tech/${teacherId}`;
192
+ const { body } = await this.authPost(url, { htype: String(period) });
193
+ const days = parseTeacherFullSchedule(body, this.educationType);
194
+ this.cache?.set("schedule", cacheKey, days);
195
+ return days;
196
+ }
197
+ async getTeacherSchedule(teacherId) {
198
+ const schedules = new Map();
199
+ const results = await Promise.all(ALL_PERIODS.map(async (period) => {
200
+ const days = await this.fetchTeacherSchedule(teacherId, period);
201
+ return { period, days };
202
+ }));
203
+ for (const { period, days } of results) {
204
+ schedules.set(period, days);
205
+ }
206
+ return new Schedule(teacherId, schedules, undefined, this.educationType, undefined, undefined, true);
207
+ }
208
+ async getTeacherScheduleForPeriod(opts) {
209
+ const days = await this.fetchTeacherSchedule(opts.teacherId, opts.period);
210
+ const schedules = new Map();
211
+ schedules.set(opts.period, days);
212
+ return new Schedule(opts.teacherId, schedules, opts.period, this.educationType, undefined, undefined, true);
213
+ }
214
+ async getTeacherInfo(teacherId) {
215
+ const url = `${BASE}/index/techtt/tech/${teacherId}`;
216
+ const { body } = await this.authGet(url);
217
+ return parseTeacherInfo(body);
218
+ }
176
219
  }
@@ -1,5 +1,5 @@
1
1
  import { type Period, EducationType } from "../common/types.js";
2
- import type { Faculty, Group, FullScheduleDay } from "./types.js";
2
+ import type { Faculty, Group, FullScheduleDay, TeacherInfo } from "./types.js";
3
3
  export declare function parsePeriodFromPage(html: string): Period | null;
4
4
  export declare function parseGroupButtons(html: string): Group[];
5
5
  export declare function parseFacultyButtons(html: string): Faculty[];
@@ -8,3 +8,5 @@ export declare function parseTeacherButtons(html: string): {
8
8
  name: string;
9
9
  }[];
10
10
  export declare function parseFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
11
+ export declare function parseTeacherFullSchedule(html: string, educationType?: EducationType): FullScheduleDay[];
12
+ export declare function parseTeacherInfo(html: string): TeacherInfo | null;
package/dist/tt/parse.js CHANGED
@@ -65,7 +65,7 @@ export function parseFullSchedule(html, educationType) {
65
65
  return parseSemesterSchedule(doc);
66
66
  }
67
67
  // --- Semester schedule parsing (weekday-based, repeating weekly) ---
68
- function parseSemesterSchedule(doc) {
68
+ function parseSemesterScheduleWith(doc, entryParser) {
69
69
  const days = [];
70
70
  const rows = doc.querySelectorAll("tr");
71
71
  let currentDay = null;
@@ -96,7 +96,7 @@ function parseSemesterSchedule(doc) {
96
96
  continue;
97
97
  const entries = [];
98
98
  for (const entryRow of dataCell.querySelectorAll("table tr")) {
99
- const entry = parseSemesterEntry(entryRow);
99
+ const entry = entryParser(entryRow);
100
100
  if (entry)
101
101
  entries.push(entry);
102
102
  }
@@ -109,6 +109,9 @@ function parseSemesterSchedule(doc) {
109
109
  }
110
110
  return days;
111
111
  }
112
+ function parseSemesterSchedule(doc) {
113
+ return parseSemesterScheduleWith(doc, parseSemesterEntry);
114
+ }
112
115
  function parseDate(dd, mm, yyyy) {
113
116
  return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
114
117
  }
@@ -131,6 +134,7 @@ function parseTransferDiv(div) {
131
134
  const parts = divHtml.split(/<br\s*\/?>/);
132
135
  const lastPart = parts[parts.length - 1]?.replace(/<[^>]*>/g, "").trim();
133
136
  const transfer = { targetDate, fromDate, fromSlot, subject };
137
+ const subgroupMatch = divText.match(/(\d+)\s*подгруппа/);
134
138
  return {
135
139
  transfer,
136
140
  entry: {
@@ -139,6 +143,7 @@ function parseTransferDiv(div) {
139
143
  type: typeMatch?.[1] ?? "",
140
144
  weeks: { from: 0, to: 0 },
141
145
  teacher: parseTeacher(lastPart ?? ""),
146
+ subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
142
147
  transfer,
143
148
  },
144
149
  };
@@ -160,6 +165,47 @@ function parseSubstitutionDiv(div) {
160
165
  teacher = parseTeacher(teacherMatch[1].trim());
161
166
  return { date, room, teacher };
162
167
  }
168
+ function parseSubstituteForDiv(div) {
169
+ const divText = text(div);
170
+ const divHtml = div.innerHTML ?? "";
171
+ const m = divText.match(/(\d{2})\.(\d{2})\.(\d{4})\s*замена\s*вместо:/);
172
+ if (!m)
173
+ return null;
174
+ const date = parseDate(m[1], m[2], m[3]);
175
+ // Original teacher: first blue span (right after "замена вместо:")
176
+ const origTeacherMatch = divHtml.match(/замена\s*вместо:\s*<\/b><\/span>\s*<span[^>]*>([^<]+)<\/span>/);
177
+ const originalTeacher = origTeacherMatch
178
+ ? parseTeacher(origTeacherMatch[1].trim())
179
+ : { name: "" };
180
+ // Subject: second blue span
181
+ const subjectEl = div.querySelectorAll('span[style*="color: blue"]');
182
+ let subject = "";
183
+ for (const el of subjectEl) {
184
+ const t = text(el);
185
+ if (t && t !== origTeacherMatch?.[1]?.trim()) {
186
+ subject = t;
187
+ break;
188
+ }
189
+ }
190
+ if (!subject)
191
+ return null;
192
+ const roomMatch = divHtml.match(/(?:<br\s*\/?>)\s*([А-Яа-яA-Za-z]-\d+)/);
193
+ const typeMatch = divText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
194
+ const groupsMatch = divHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс)\)\s*(?:<br\s*\/?>)\s*([^<]+?)(?:\s*<i|$)/);
195
+ const subgroupMatch = divText.match(/(\d+)\s*подгруппа/);
196
+ return {
197
+ entry: {
198
+ room: roomMatch?.[1] ?? "",
199
+ subject,
200
+ type: typeMatch?.[1] ?? "",
201
+ weeks: { from: 0, to: 0 },
202
+ teacher: { name: "" },
203
+ groups: groupsMatch?.[1]?.trim() ?? "",
204
+ subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
205
+ substituteFor: { date, originalTeacher },
206
+ },
207
+ };
208
+ }
163
209
  function parseSemesterEntry(el) {
164
210
  const td = el.querySelector("td") ?? el;
165
211
  const fullHtml = td.innerHTML ?? "";
@@ -291,3 +337,158 @@ function parseSessionEntry(td) {
291
337
  timeEnd: parseTime(timeMatch[2]),
292
338
  };
293
339
  }
340
+ // --- Teacher schedule parsing ---
341
+ export function parseTeacherFullSchedule(html, educationType) {
342
+ const doc = parseHtml(html);
343
+ const edType = educationType ?? 1 /* EducationType.HigherEducation */;
344
+ if (doc.querySelector('td[id^="trd2"]')) {
345
+ return parseTeacherSessionSchedule(doc, edType);
346
+ }
347
+ return parseSemesterScheduleWith(doc, parseTeacherSemesterEntry);
348
+ }
349
+ function parseTeacherSemesterEntry(el) {
350
+ const td = el.querySelector("td") ?? el;
351
+ const fullHtml = td.innerHTML ?? "";
352
+ const plainText = text(td);
353
+ if (!plainText)
354
+ return null;
355
+ const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
356
+ const redDivs = td.querySelectorAll('div[style*="border: 2px solid red"]');
357
+ for (const div of redDivs) {
358
+ const result = parseTransferDiv(div);
359
+ if (result) {
360
+ if (possibleChanges)
361
+ result.entry.possibleChanges = true;
362
+ return result.entry;
363
+ }
364
+ }
365
+ // Check for "замена вместо:" (substitute lesson for another teacher)
366
+ for (const div of redDivs) {
367
+ const result = parseSubstituteForDiv(div);
368
+ if (result) {
369
+ if (possibleChanges)
370
+ result.entry.possibleChanges = true;
371
+ return result.entry;
372
+ }
373
+ }
374
+ const substitutions = [];
375
+ for (const div of redDivs) {
376
+ const sub = parseSubstitutionDiv(div);
377
+ if (sub)
378
+ substitutions.push(sub);
379
+ }
380
+ let cleanHtml = fullHtml;
381
+ let cleanText = plainText;
382
+ for (const div of redDivs) {
383
+ cleanHtml = cleanHtml.replace(div.outerHTML ?? "", "");
384
+ cleanText = cleanText.replace(text(div), "");
385
+ }
386
+ const subjectEl = td.querySelector('span[style*="color: blue"]');
387
+ const subject = subjectEl ? text(subjectEl) : "";
388
+ if (!subject)
389
+ return null;
390
+ const typeMatch = cleanText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс)\)/);
391
+ const weeksMatch = cleanText.match(/\(([^)]*нед\.?[^)]*)\)/);
392
+ const roomMatch = cleanHtml.match(/(?:<sup>[^<]*<\/sup>)?([А-Яа-яA-Za-z]-\d+)/);
393
+ const groupsMatch = cleanHtml.match(/<br\s*\/?>\s*([^<]+?)(?:<br|<\/td|<div|<i|$)/);
394
+ const subgroupMatch = cleanText.match(/(\d+)\s*подгруппа/);
395
+ const weekParity = parseWeekParity(cleanHtml);
396
+ return {
397
+ room: roomMatch?.[1] ?? "",
398
+ subject,
399
+ type: typeMatch?.[1] ?? "",
400
+ weeks: parseWeeks(weeksMatch?.[1] ?? ""),
401
+ teacher: { name: "" },
402
+ groups: groupsMatch?.[1]?.trim() ?? "",
403
+ subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
404
+ weekParity,
405
+ substitutions: substitutions.length > 0 ? substitutions : undefined,
406
+ possibleChanges,
407
+ };
408
+ }
409
+ function parseTeacherSessionSchedule(doc, educationType) {
410
+ const days = [];
411
+ for (const dateCell of doc.querySelectorAll('td[id^="trd2"]')) {
412
+ const id = dateCell.getAttribute("id") ?? "";
413
+ const dateMatch = id.match(/trd(\d{4})(\d{2})(\d{2})/);
414
+ if (!dateMatch)
415
+ continue;
416
+ const year = parseInt(dateMatch[1]);
417
+ const month = parseInt(dateMatch[2]) - 1;
418
+ const dayNum = parseInt(dateMatch[3]);
419
+ const date = new Date(year, month, dayNum);
420
+ const cellHtml = dateCell.innerHTML ?? "";
421
+ const brMatch = cellHtml.match(/<br\s*\/?>\s*(.+)/i);
422
+ const weekday = brMatch ? brMatch[1].trim() : "";
423
+ const row = dateCell.parentElement;
424
+ if (!row)
425
+ continue;
426
+ const dataCell = row.querySelector("td.trdata:not(.trfd)");
427
+ if (!dataCell)
428
+ continue;
429
+ const slots = [];
430
+ for (const entryRow of dataCell.querySelectorAll("table tr")) {
431
+ const td = entryRow.querySelector("td") ?? entryRow;
432
+ const entry = parseTeacherSessionEntry(td);
433
+ if (!entry)
434
+ continue;
435
+ slots.push({
436
+ number: getLessonNumber(entry.timeStart, educationType),
437
+ timeStart: entry.timeStart,
438
+ timeEnd: entry.timeEnd,
439
+ entries: [entry.entry],
440
+ });
441
+ }
442
+ if (slots.length > 0) {
443
+ days.push({ weekday, date, slots });
444
+ }
445
+ }
446
+ return days;
447
+ }
448
+ function parseTeacherSessionEntry(td) {
449
+ const fullHtml = td.innerHTML ?? "";
450
+ const plainText = text(td);
451
+ if (!plainText)
452
+ return null;
453
+ const subjectEl = td.querySelector('span[style*="color: blue"]');
454
+ const subject = subjectEl ? text(subjectEl) : "";
455
+ if (!subject)
456
+ return null;
457
+ const roomMatch = fullHtml.match(/^([^<]*?)\s*<span/);
458
+ const room = roomMatch ? roomMatch[1].trim() : "";
459
+ const typeMatch = plainText.match(/\((лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)/i);
460
+ const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
461
+ // Groups: text between </span> type and <br>time
462
+ const groupsMatch = fullHtml.match(/\((?:лк|пр|лб|зач|экз|зчО|кр|конс\.?|Экз)\)\s*([^<]+?)\s*<br/i);
463
+ const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
464
+ if (!timeMatch)
465
+ return null;
466
+ return {
467
+ entry: {
468
+ room,
469
+ subject,
470
+ type,
471
+ weeks: { from: 0, to: 0 },
472
+ teacher: { name: "" },
473
+ groups: groupsMatch?.[1]?.trim() ?? "",
474
+ },
475
+ timeStart: parseTime(timeMatch[1]),
476
+ timeEnd: parseTime(timeMatch[2]),
477
+ };
478
+ }
479
+ export function parseTeacherInfo(html) {
480
+ const doc = parseHtml(html);
481
+ const nameEl = doc.querySelector(".htextb");
482
+ if (!nameEl)
483
+ return null;
484
+ const nameHtml = nameEl.innerHTML ?? "";
485
+ const nameMatch = nameHtml.match(/^([^<]+)/);
486
+ const name = nameMatch?.[1]?.trim() ?? "";
487
+ if (!name)
488
+ return null;
489
+ const degreeEl = nameEl.querySelector('span[style*="color: blue"]');
490
+ const degree = degreeEl ? text(degreeEl).trim() : undefined;
491
+ const deptEl = doc.querySelector(".htext");
492
+ const department = deptEl ? text(deptEl).trim() : undefined;
493
+ return { name, degree: degree || undefined, department: department || undefined };
494
+ }
@@ -5,12 +5,14 @@ export declare class Schedule {
5
5
  readonly groupId: number;
6
6
  readonly scheduleMap: Map<number, FullScheduleDay[]>;
7
7
  readonly educationType: EducationType;
8
+ /** Whether this is a teacher schedule (affects substitution handling). */
9
+ readonly isTeacherSchedule: boolean;
8
10
  /** List of holidays to exclude from schedule queries. Pass `[]` to disable. */
9
11
  readonly holidays: Holiday[];
10
12
  /** Government decree day-off transfers (Постановление Правительства). */
11
13
  readonly holidayTransfers: HolidayTransfer[];
12
14
  private _period?;
13
- constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null, holidayTransfers?: HolidayTransfer[]);
15
+ constructor(groupId: number, scheduleMap: Map<number, FullScheduleDay[]>, period?: Period, educationType?: EducationType, holidays?: Holiday[] | null, holidayTransfers?: HolidayTransfer[], isTeacherSchedule?: boolean);
14
16
  /** Current (or fixed) period for this schedule. */
15
17
  get period(): Period;
16
18
  /** Days for the current period. */
@@ -3,15 +3,18 @@ export class Schedule {
3
3
  groupId;
4
4
  scheduleMap;
5
5
  educationType;
6
+ /** Whether this is a teacher schedule (affects substitution handling). */
7
+ isTeacherSchedule;
6
8
  /** List of holidays to exclude from schedule queries. Pass `[]` to disable. */
7
9
  holidays;
8
10
  /** Government decree day-off transfers (Постановление Правительства). */
9
11
  holidayTransfers;
10
12
  _period;
11
- constructor(groupId, scheduleMap, period, educationType, holidays, holidayTransfers) {
13
+ constructor(groupId, scheduleMap, period, educationType, holidays, holidayTransfers, isTeacherSchedule) {
12
14
  this.groupId = groupId;
13
15
  this.scheduleMap = scheduleMap;
14
16
  this.educationType = educationType ?? 1 /* EducationType.HigherEducation */;
17
+ this.isTeacherSchedule = isTeacherSchedule ?? false;
15
18
  this._period = period;
16
19
  this.holidays = holidays ?? RUSSIAN_HOLIDAYS;
17
20
  this.holidayTransfers = holidayTransfers ?? [];
@@ -73,14 +76,14 @@ export class Schedule {
73
76
  const dayName = getWeekdayName(weekday);
74
77
  for (const d of days) {
75
78
  if (d.weekday.toLowerCase() === dayName.toLowerCase() && d.date) {
76
- lessons.push(...slotsToLessons(d.slots, d.date));
79
+ lessons.push(...slotsToLessons(d.slots, d.date, { isTeacherSchedule: this.isTeacherSchedule }));
77
80
  }
78
81
  }
79
82
  return lessons.sort(sortLessons);
80
83
  }
81
84
  const slots = this.getSlotsForWeekday(weekday, days, opts);
82
85
  const date = this.getDateForWeekday(weekday, period, opts?.week);
83
- return slotsToLessons(slots, date);
86
+ return slotsToLessons(slots, date, { isTeacherSchedule: this.isTeacherSchedule });
84
87
  }
85
88
  forDate(date, opts) {
86
89
  if (isHoliday(date, this.holidays, this.holidayTransfers))
@@ -91,7 +94,7 @@ export class Schedule {
91
94
  for (const [, d] of this.scheduleMap) {
92
95
  for (const day of d) {
93
96
  if (day.date && Schedule.isSameDay(day.date, date)) {
94
- lessons.push(...slotsToLessons(day.slots, date));
97
+ lessons.push(...slotsToLessons(day.slots, date, { isTeacherSchedule: this.isTeacherSchedule }));
95
98
  }
96
99
  }
97
100
  }
@@ -109,7 +112,7 @@ export class Schedule {
109
112
  week,
110
113
  date,
111
114
  });
112
- lessons.push(...slotsToLessons(slots, date));
115
+ lessons.push(...slotsToLessons(slots, date, { isTeacherSchedule: this.isTeacherSchedule }));
113
116
  }
114
117
  // 3. Suppress lessons that were transferred away from this date
115
118
  const transfers = collectTransfers(semesterDays);
@@ -18,6 +18,13 @@ export interface Substitution {
18
18
  /** New teacher, if changed. */
19
19
  teacher?: Teacher;
20
20
  }
21
+ /** Info about a lesson this teacher is substituting for another teacher. */
22
+ export interface SubstituteForInfo {
23
+ /** The date this substitute lesson takes place. */
24
+ date: Date;
25
+ /** The original teacher being replaced. */
26
+ originalTeacher: Teacher;
27
+ }
21
28
  /** Info about a lesson transferred from another date/slot. */
22
29
  export interface TransferInfo {
23
30
  /** Date when this lesson takes place (target). */
@@ -35,12 +42,16 @@ export interface ScheduleEntry {
35
42
  type: string;
36
43
  weeks: WeekRange;
37
44
  teacher: Teacher;
45
+ /** For teacher schedules: group names (e.g. "КТ-42-25 (1 подгруппа)"). */
46
+ groups?: string;
38
47
  subgroup?: number;
39
48
  weekParity?: "even" | "odd";
40
49
  /** Date-specific substitutions (замена на). */
41
50
  substitutions?: Substitution[];
42
51
  /** If this entry is a transferred lesson (перенос). */
43
52
  transfer?: TransferInfo;
53
+ /** If this entry is a substitute lesson (замена вместо). */
54
+ substituteFor?: SubstituteForInfo;
44
55
  /** Whether this entry is marked as potentially changing (class="want"). */
45
56
  possibleChanges?: boolean;
46
57
  }
@@ -73,6 +84,8 @@ export interface Lesson {
73
84
  type: string;
74
85
  room: string;
75
86
  teacher: Teacher;
87
+ /** For teacher schedules: group names. */
88
+ groups?: string;
76
89
  weeks: WeekRange;
77
90
  subgroup?: number;
78
91
  weekParity?: "even" | "odd";
@@ -82,9 +95,17 @@ export interface Lesson {
82
95
  originalTeacher?: Teacher;
83
96
  /** Transfer info if this lesson was moved from another date/slot. */
84
97
  transfer?: TransferInfo;
98
+ /** If this lesson is a substitute (замена вместо), the original teacher. */
99
+ substituteFor?: SubstituteForInfo;
85
100
  /** Whether this lesson is marked as potentially changing. */
86
101
  possibleChanges?: boolean;
87
102
  }
103
+ /** Teacher info from the schedule page header. */
104
+ export interface TeacherInfo {
105
+ name: string;
106
+ degree?: string;
107
+ department?: string;
108
+ }
88
109
  export interface SemesterWeek {
89
110
  week: number;
90
111
  start: Date;
@@ -94,6 +115,7 @@ export interface CacheConfig {
94
115
  schedule?: number;
95
116
  faculties?: number;
96
117
  groups?: number;
118
+ teachers?: number;
97
119
  }
98
120
  export interface TtClientOptions {
99
121
  educationType?: EducationType;
@@ -38,7 +38,9 @@ export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
38
38
  week?: number;
39
39
  date?: Date;
40
40
  }): FullScheduleSlot[];
41
- export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date): Lesson[];
41
+ export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date, opts?: {
42
+ isTeacherSchedule?: boolean;
43
+ }): Lesson[];
42
44
  /** Collect all transfer entries from schedule days. */
43
45
  export declare function collectTransfers(days: FullScheduleDay[]): ScheduleEntry[];
44
46
  /** Remove lessons whose source date/slot match a transfer. */
package/dist/tt/utils.js CHANGED
@@ -101,6 +101,12 @@ function filterEntries(entries, opts) {
101
101
  return false;
102
102
  return isSameDay(e.transfer.targetDate, opts.date);
103
103
  }
104
+ // Substitute-for entries: only include when the query date matches
105
+ if (e.substituteFor) {
106
+ if (!opts?.date)
107
+ return false;
108
+ return isSameDay(e.substituteFor.date, opts.date);
109
+ }
104
110
  if (opts?.subgroup && e.subgroup && e.subgroup !== opts.subgroup) {
105
111
  return false;
106
112
  }
@@ -135,7 +141,7 @@ function makeLessonTime(date, time) {
135
141
  d.setHours(time.hours, time.minutes, 0, 0);
136
142
  return { date: d, hours: time.hours, minutes: time.minutes };
137
143
  }
138
- export function slotsToLessons(slots, date) {
144
+ export function slotsToLessons(slots, date, opts) {
139
145
  const lessons = [];
140
146
  for (const slot of slots) {
141
147
  for (const entry of slot.entries) {
@@ -152,6 +158,10 @@ export function slotsToLessons(slots, date) {
152
158
  room = sub.room;
153
159
  }
154
160
  if (sub.teacher) {
161
+ // On teacher schedules, a teacher substitution means another teacher
162
+ // is taking over — exclude the lesson entirely.
163
+ if (opts?.isTeacherSchedule)
164
+ continue;
155
165
  originalTeacher = teacher;
156
166
  teacher = sub.teacher;
157
167
  }
@@ -165,12 +175,14 @@ export function slotsToLessons(slots, date) {
165
175
  type: entry.type,
166
176
  room,
167
177
  teacher,
178
+ groups: entry.groups,
168
179
  weeks: entry.weeks,
169
180
  subgroup: entry.subgroup,
170
181
  weekParity: entry.weekParity,
171
182
  originalRoom,
172
183
  originalTeacher,
173
184
  transfer: entry.transfer,
185
+ substituteFor: entry.substituteFor,
174
186
  possibleChanges: entry.possibleChanges,
175
187
  });
176
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chuvsu-js",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "description": "Node.js library for ChuvSU student portal (lk.chuvsu.ru) and schedule (tt.chuvsu.ru)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",