@lime1/esprit-ts 1.0.0 → 1.1.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/README.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  A TypeScript client library for the ESPRIT student portal. Scrape grades, absences, credits, schedules, and student information with full type safety.
4
4
 
5
+ ## Features
6
+
7
+ - 🔐 **Authentication**: Secure login/logout with session management
8
+ - 📊 **Academic Data**:
9
+ - Regular grades (incremental release during semester)
10
+ - Principal session results (final verdict)
11
+ - Rattrapage grades (retake session incremental)
12
+ - Rattrapage session results (retake final verdict)
13
+ - Historical ranking across multiple years
14
+ - Language proficiency levels (French & English)
15
+ - 📅 **Attendance**: Absence tracking
16
+ - 🎓 **Credits**: Academic credit history
17
+ - 📋 **Schedules**: Class schedules with download links
18
+ - 👤 **Student Info**: Name and class information
19
+ - 🔄 **ASP.NET Support**: Full handling of complex ASP.NET postback mechanisms
20
+ - 📦 **Type Safety**: Complete TypeScript definitions
21
+
5
22
  ## Installation
6
23
 
7
24
  ```bash
@@ -14,56 +31,117 @@ yarn add @lime1/esprit-ts
14
31
 
15
32
  ## Usage
16
33
 
34
+ ### Basic Example
35
+
17
36
  ```typescript
18
37
  import { EspritClient } from '@lime1/esprit-ts';
19
38
 
20
-
21
39
  async function main() {
22
- // Create client (optionally enable debug mode)
23
40
  const client = new EspritClient({ debug: false });
24
41
 
25
- // Login with your ESPRIT credentials
42
+ // Login
26
43
  const result = await client.login('your-student-id', 'your-password');
27
-
28
44
  if (!result.success) {
29
45
  console.error('Login failed:', result.message);
30
46
  return;
31
47
  }
32
48
 
33
- console.log('Login successful!');
34
-
35
49
  // Get student info
36
50
  const info = await client.getStudentInfo();
37
- console.log(`Name: ${info.name}`);
38
- console.log(`Class: ${info.className}`);
39
-
40
- // Get grades
41
- const grades = await client.getGrades();
42
- console.log('Grades:', grades);
51
+ console.log(`${info.name} - ${info.className}`);
52
+
53
+ // Get regular grades (released during semester)
54
+ const regularGrades = await client.getRegularGrades();
55
+ if (regularGrades) {
56
+ const [headers, ...rows] = regularGrades;
57
+ console.log('Headers:', headers);
58
+ console.log('Grades:', rows);
59
+ }
43
60
 
44
- // Get grades with calculated averages
45
- const summary = await client.getGradesWithAverage();
46
- if (summary) {
47
- console.log(`Total Average: ${summary.totalAverage?.toFixed(2)}`);
61
+ // Get principal session result (final verdict)
62
+ const principalResult = await client.getPrincipalResult();
63
+ if (principalResult) {
64
+ console.log(`Average: ${principalResult.moyenneGeneral}`);
65
+ console.log(`Decision: ${principalResult.decision}`);
48
66
  }
49
67
 
50
- // Get absences
68
+ await client.logout();
69
+ }
70
+
71
+ main().catch(console.error);
72
+ ```
73
+
74
+ ### Comprehensive Example
75
+
76
+ ```typescript
77
+ import { EspritClient } from '@lime1/esprit-ts';
78
+
79
+ async function fetchAllData() {
80
+ const client = new EspritClient({ debug: true });
81
+
82
+ const login = await client.login('your-id', 'your-password');
83
+ if (!login.success) return;
84
+
85
+ console.log('=== Student Info ===');
86
+ const info = await client.getStudentInfo();
87
+ console.log(info);
88
+
89
+ console.log('\n=== Regular Grades ===');
90
+ const regularGrades = await client.getRegularGrades();
91
+ console.log(regularGrades);
92
+
93
+ console.log('\n=== Principal Result ===');
94
+ const principalResult = await client.getPrincipalResult();
95
+ console.log(principalResult);
96
+
97
+ console.log('\n=== Rattrapage Grades ===');
98
+ const rattrapageGrades = await client.getRattrapageGrades();
99
+ console.log(rattrapageGrades);
100
+
101
+ console.log('\n=== Rattrapage Result ===');
102
+ const rattrapageResult = await client.getRattrapageResult();
103
+ console.log(rattrapageResult);
104
+
105
+ console.log('\n=== Language Levels ===');
106
+ const languageLevels = await client.getLanguageLevels();
107
+ console.log(languageLevels);
108
+
109
+ console.log('\n=== Ranking ===');
110
+ const ranking = await client.getRanking();
111
+ console.log(ranking);
112
+
113
+ console.log('\n=== Absences ===');
51
114
  const absences = await client.getAbsences();
52
- console.log('Absences:', absences);
115
+ console.log(absences);
53
116
 
54
- // Get credit history
117
+ console.log('\n=== Credits ===');
55
118
  const credits = await client.getCredits();
56
- console.log('Credits:', credits);
119
+ console.log(credits);
57
120
 
58
- // Get schedules
121
+ console.log('\n=== Schedules ===');
59
122
  const schedules = await client.getSchedules();
60
- console.log('Schedules:', schedules);
123
+ console.log(schedules);
61
124
 
62
- // Logout when done
63
125
  await client.logout();
64
126
  }
65
127
 
66
- main().catch(console.error);
128
+ fetchAllData().catch(console.error);
129
+ ```
130
+
131
+ ### Legacy Grade Format with Averages
132
+
133
+ ```typescript
134
+ // For backward compatibility, the old getGrades() method still works
135
+ // but returns parsed Grade objects instead of raw data
136
+ const grades = await client.getGrades(); // deprecated
137
+ const summary = await client.getGradesWithAverage();
138
+
139
+ if (summary) {
140
+ console.log(`Total Average: ${summary.totalAverage?.toFixed(2)}`);
141
+ summary.grades.forEach(grade => {
142
+ console.log(`${grade.designation}: ${grade.moyenne?.toFixed(2)}`);
143
+ });
144
+ }
67
145
  ```
68
146
 
69
147
  ## API Reference
@@ -76,31 +154,47 @@ main().catch(console.error);
76
154
  new EspritClient(config?: EspritClientConfig)
77
155
  ```
78
156
 
79
- | Option | Type | Default | Description |
80
- |--------|------|---------|-------------|
81
- | `debug` | `boolean` | `false` | Enable debug logging |
82
- | `userAgent` | `string` | Chrome UA | Custom user agent string |
83
- | `timeout` | `number` | `30000` | Request timeout in ms |
157
+ | Option | Type | Default | Description |
158
+ | ----------- | --------- | --------- | ------------------------ |
159
+ | `debug` | `boolean` | `false` | Enable debug logging |
160
+ | `userAgent` | `string` | Chrome UA | Custom user agent string |
161
+ | `timeout` | `number` | `30000` | Request timeout in ms |
84
162
 
85
163
  #### Methods
86
164
 
87
- | Method | Returns | Description |
88
- |--------|---------|-------------|
89
- | `login(id, password)` | `Promise<LoginResult>` | Login to the portal |
90
- | `logout()` | `Promise<boolean>` | Logout from the portal |
91
- | `getGrades()` | `Promise<Grade[] \| null>` | Get student grades |
92
- | `getGradesWithAverage()` | `Promise<GradeSummary \| null>` | Get grades with calculated averages |
93
- | `getAbsences()` | `Promise<Absence[] \| null>` | Get student absences |
94
- | `getCredits()` | `Promise<Credit[] \| null>` | Get credit history |
95
- | `getSchedules()` | `Promise<Schedule[] \| null>` | Get available schedules |
96
- | `getStudentName()` | `Promise<string \| null>` | Get student name |
97
- | `getStudentClass()` | `Promise<string \| null>` | Get student class |
98
- | `getStudentInfo()` | `Promise<StudentInfo>` | Get name and class |
99
- | `getCookies()` | `Promise<Cookie[]>` | Get session cookies |
165
+ ##### Authentication
166
+ | Method | Returns | Description |
167
+ | --------------------- | ---------------------- | ---------------------- |
168
+ | `login(id, password)` | `Promise<LoginResult>` | Login to the portal |
169
+ | `logout()` | `Promise<boolean>` | Logout from the portal |
170
+ | `getCookies()` | `Promise<Cookie[]>` | Get session cookies |
171
+
172
+ ##### Grades & Results
173
+ | Method | Returns | Description |
174
+ | ------------------------ | ----------------------------------- | -------------------------------------------------------- |
175
+ | `getRegularGrades()` | `Promise<RegularGrade \| null>` | Get grades released during semester (Session Principale) |
176
+ | `getPrincipalResult()` | `Promise<PrincipalResult \| null>` | Get final verdict of principal session |
177
+ | `getRattrapageGrades()` | `Promise<RattrapageGrade \| null>` | Get retake exam grades (incremental) |
178
+ | `getRattrapageResult()` | `Promise<RattrapageResult \| null>` | Get final verdict of rattrapage session |
179
+ | `getLanguageLevels()` | `Promise<LanguageLevel \| null>` | Get French and English proficiency levels |
180
+ | `getRanking()` | `Promise<RankingEntry[] \| null>` | Get historical academic ranking |
181
+ | `getGrades()` | `Promise<Grade[] \| null>` | **DEPRECATED:** Use `getRegularGrades()` instead |
182
+ | `getGradesWithAverage()` | `Promise<GradeSummary \| null>` | Get parsed grades with calculated averages |
183
+
184
+ ##### Other Data
185
+ | Method | Returns | Description |
186
+ | ------------------- | ----------------------------- | --------------------------- |
187
+ | `getAbsences()` | `Promise<Absence[] \| null>` | Get student absences |
188
+ | `getCredits()` | `Promise<Credit[] \| null>` | Get credit history |
189
+ | `getSchedules()` | `Promise<Schedule[] \| null>` | Get available schedules |
190
+ | `getStudentName()` | `Promise<string \| null>` | Get student name |
191
+ | `getStudentClass()` | `Promise<string \| null>` | Get student class |
192
+ | `getStudentInfo()` | `Promise<StudentInfo>` | Get name and class together |
100
193
 
101
194
  ### Types
102
195
 
103
196
  ```typescript
197
+ // Legacy parsed grade format (used by getGrades())
104
198
  interface Grade {
105
199
  designation: string;
106
200
  coefficient: number | null;
@@ -115,6 +209,32 @@ interface GradeSummary {
115
209
  totalCoefficient: number;
116
210
  }
117
211
 
212
+ // New grade formats (raw table data)
213
+ type RegularGrade = string[][]; // First row is headers
214
+ type RattrapageGrade = string[][]; // First row is headers
215
+
216
+ interface PrincipalResult {
217
+ moyenneGeneral: string | null;
218
+ decision: string | null;
219
+ }
220
+
221
+ interface RattrapageResult {
222
+ moyenneGeneral: string | null;
223
+ decision: string | null;
224
+ }
225
+
226
+ interface LanguageLevel {
227
+ francais: string | null;
228
+ anglais: string | null;
229
+ }
230
+
231
+ interface RankingEntry {
232
+ anneeUniversitaire: string | null;
233
+ classe: string | null;
234
+ moyenne: string | null;
235
+ rang: string | null;
236
+ }
237
+
118
238
  interface LoginResult {
119
239
  success: boolean;
120
240
  cookies: Cookie[];
package/dist/index.cjs CHANGED
@@ -173,6 +173,13 @@ var EspritClient = class {
173
173
  ABSENCES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/absenceetud.aspx";
174
174
  CREDITS_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Historique_Cr%C3%A9dit.aspx";
175
175
  SCHEDULES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Emplois.aspx";
176
+ // New grade endpoints
177
+ REGULAR_GRADES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/Resultat2021.aspx";
178
+ PRINCIPAL_RESULT_URL = "https://esprit-tn.com/ESPOnline/Etudiants/ResultatPrincipale2021.aspx";
179
+ RATTRAPAGE_GRADES_URL = "https://esprit-tn.com/ESPOnline/Etudiants/noterat.aspx";
180
+ RATTRAPAGE_RESULT_URL = "https://esprit-tn.com/ESPOnline/Etudiants/ResultatRattrapage2021.aspx";
181
+ LANGUAGE_LEVELS_URL = "https://esprit-tn.com/ESPOnline/Etudiants/LANG2.aspx";
182
+ RANKING_URL = "https://esprit-tn.com/ESPOnline/Etudiants/ranking.aspx";
176
183
  LOGOUT_URLS = [
177
184
  "https://esprit-tn.com/esponline/Etudiants/Deconnexion.aspx",
178
185
  "https://esprit-tn.com/esponline/online/Deconnexion.aspx"
@@ -408,6 +415,7 @@ var EspritClient = class {
408
415
  /**
409
416
  * Get student grades.
410
417
  *
418
+ * @deprecated Use getRegularGrades() for more accurate naming. This method will be removed in a future version.
411
419
  * @returns Array of Grade objects or null if page cannot be loaded
412
420
  */
413
421
  async getGrades() {
@@ -685,6 +693,292 @@ var EspritClient = class {
685
693
  return { name: null, className: null };
686
694
  }
687
695
  }
696
+ /**
697
+ * Get regular grades (grades released incrementally during the semester - Session Principale).
698
+ *
699
+ * This replaces the old getGrades() method with more accurate naming.
700
+ * Fetches data from Resultat2021.aspx which displays module grades as they're released.
701
+ *
702
+ * @returns Array of string arrays where first row is headers, or null if unavailable
703
+ */
704
+ async getRegularGrades() {
705
+ try {
706
+ const response = await this.client.get(this.REGULAR_GRADES_URL, { maxRedirects: 5 });
707
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
708
+ this.log("Session expired - redirected to login");
709
+ return null;
710
+ }
711
+ if (response.data.includes("aucune note!")) {
712
+ this.log("No grades available yet");
713
+ return null;
714
+ }
715
+ const $ = cheerio2.load(response.data);
716
+ const h1Tag = $("h1").filter((_, el) => {
717
+ const text = $(el).text();
718
+ return text.includes("Notes Des Modules");
719
+ });
720
+ if (h1Tag.length === 0) {
721
+ this.log("Page does not contain expected content");
722
+ return null;
723
+ }
724
+ const table = $("#ContentPlaceHolder1_GridView1");
725
+ if (table.length === 0) {
726
+ this.log("Grades table not found");
727
+ return null;
728
+ }
729
+ const { headers, rows } = parseTable($, table);
730
+ return [headers, ...rows];
731
+ } catch (error) {
732
+ this.log("Error getting regular grades:", error);
733
+ return null;
734
+ }
735
+ }
736
+ /**
737
+ * Get the final verdict/decision at the end of the principal session.
738
+ *
739
+ * Fetches data from ResultatPrincipale2021.aspx showing the overall average and pass/fail decision.
740
+ *
741
+ * @returns PrincipalResult with moyenneGeneral and decision, or null if unavailable
742
+ */
743
+ async getPrincipalResult() {
744
+ try {
745
+ const response = await this.client.get(this.PRINCIPAL_RESULT_URL, { maxRedirects: 5 });
746
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
747
+ this.log("Session expired - redirected to login");
748
+ return null;
749
+ }
750
+ const $ = cheerio2.load(response.data);
751
+ const h1Tag = $("h1").filter((_, el) => {
752
+ const text = $(el).text();
753
+ return text.includes("Resultat Session Principale");
754
+ });
755
+ if (h1Tag.length === 0) {
756
+ this.log("Page does not contain expected content");
757
+ return null;
758
+ }
759
+ const table = $("#ContentPlaceHolder1_GridView3");
760
+ if (table.length === 0) {
761
+ this.log("Result table not found");
762
+ return null;
763
+ }
764
+ const { headers, rows } = parseTable($, table);
765
+ if (rows.length === 0) {
766
+ this.log("No result data found");
767
+ return null;
768
+ }
769
+ const headerLower = headers.map((h) => h.toLowerCase());
770
+ const moyenneIdx = headerLower.findIndex((h) => h.includes("moyenne"));
771
+ const decisionIdx = headerLower.findIndex((h) => h.includes("decision"));
772
+ return {
773
+ moyenneGeneral: moyenneIdx >= 0 ? rows[0][moyenneIdx] ?? null : null,
774
+ decision: decisionIdx >= 0 ? rows[0][decisionIdx] ?? null : null
775
+ };
776
+ } catch (error) {
777
+ this.log("Error getting principal result:", error);
778
+ return null;
779
+ }
780
+ }
781
+ /**
782
+ * Get rattrapage (retake) grades released incrementally during the retake session.
783
+ *
784
+ * Similar to regular grades but for the retake session, fetched from noterat.aspx.
785
+ *
786
+ * @returns Array of string arrays where first row is headers, or null if unavailable
787
+ */
788
+ async getRattrapageGrades() {
789
+ try {
790
+ const response = await this.client.get(this.RATTRAPAGE_GRADES_URL, { maxRedirects: 5 });
791
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
792
+ this.log("Session expired - redirected to login");
793
+ return null;
794
+ }
795
+ if (response.data.includes("aucune note!")) {
796
+ this.log("No rattrapage grades available yet");
797
+ return null;
798
+ }
799
+ const $ = cheerio2.load(response.data);
800
+ const h1Tag = $("h1").filter((_, el) => {
801
+ const text = $(el).text();
802
+ return text.includes("Notes Des Modules session Rattrapage");
803
+ });
804
+ if (h1Tag.length === 0) {
805
+ this.log("Page does not contain expected content");
806
+ return null;
807
+ }
808
+ const table = $("#ContentPlaceHolder1_GridView1");
809
+ if (table.length === 0) {
810
+ this.log("Rattrapage grades table not found");
811
+ return null;
812
+ }
813
+ const { headers, rows } = parseTable($, table);
814
+ return [headers, ...rows];
815
+ } catch (error) {
816
+ this.log("Error getting rattrapage grades:", error);
817
+ return null;
818
+ }
819
+ }
820
+ /**
821
+ * Get the final verdict/decision of the rattrapage (retake) session.
822
+ *
823
+ * Fetches data from ResultatRattrapage2021.aspx showing the overall average and pass/fail decision.
824
+ *
825
+ * @returns RattrapageResult with moyenneGeneral and decision, or null if unavailable
826
+ */
827
+ async getRattrapageResult() {
828
+ try {
829
+ const response = await this.client.get(this.RATTRAPAGE_RESULT_URL, { maxRedirects: 5 });
830
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
831
+ this.log("Session expired - redirected to login");
832
+ return null;
833
+ }
834
+ const $ = cheerio2.load(response.data);
835
+ const h1Tag = $("h1").filter((_, el) => {
836
+ const text = $(el).text();
837
+ return text.includes("Resultat");
838
+ });
839
+ const spanTag = $("#ContentPlaceHolder1_Label18").filter((_, el) => {
840
+ const text = $(el).text();
841
+ return text.includes("Resultat");
842
+ });
843
+ if (h1Tag.length === 0 && spanTag.length === 0) {
844
+ this.log("Page does not contain expected content");
845
+ return null;
846
+ }
847
+ const table = $("#ContentPlaceHolder1_GridView3");
848
+ if (table.length === 0) {
849
+ this.log("Rattrapage result table not found");
850
+ return null;
851
+ }
852
+ const { headers, rows } = parseTable($, table);
853
+ if (rows.length === 0) {
854
+ this.log("No rattrapage result data found");
855
+ return null;
856
+ }
857
+ const headerLower = headers.map((h) => h.toLowerCase());
858
+ const moyenneIdx = headerLower.findIndex((h) => h.includes("moyenne"));
859
+ const decisionIdx = headerLower.findIndex((h) => h.includes("decision"));
860
+ return {
861
+ moyenneGeneral: moyenneIdx >= 0 ? rows[0][moyenneIdx] ?? null : null,
862
+ decision: decisionIdx >= 0 ? rows[0][decisionIdx] ?? null : null
863
+ };
864
+ } catch (error) {
865
+ this.log("Error getting rattrapage result:", error);
866
+ return null;
867
+ }
868
+ }
869
+ /**
870
+ * Get language proficiency levels (French and English).
871
+ *
872
+ * Fetches data from LANG2.aspx showing language levels like 'B1', 'B2', etc.
873
+ *
874
+ * @returns LanguageLevel with francais and anglais levels, or null if unavailable
875
+ */
876
+ async getLanguageLevels() {
877
+ try {
878
+ const response = await this.client.get(this.LANGUAGE_LEVELS_URL, { maxRedirects: 5 });
879
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
880
+ this.log("Session expired - redirected to login");
881
+ return null;
882
+ }
883
+ const $ = cheerio2.load(response.data);
884
+ const h1Tag = $("h1").filter((_, el) => {
885
+ const text = $(el).text();
886
+ return text.includes("NIVEAU LANGUES");
887
+ });
888
+ if (h1Tag.length === 0) {
889
+ this.log("Page does not contain expected content");
890
+ return null;
891
+ }
892
+ const table = $("#ContentPlaceHolder1_GridView2");
893
+ if (table.length === 0) {
894
+ this.log("Language levels table not found");
895
+ return null;
896
+ }
897
+ const { headers, rows } = parseTable($, table);
898
+ if (rows.length === 0) {
899
+ this.log("No language level data found");
900
+ return null;
901
+ }
902
+ const headerLower = headers.map((h) => h.toLowerCase());
903
+ const francaisIdx = headerLower.findIndex((h) => h.includes("francais"));
904
+ const anglaisIdx = headerLower.findIndex((h) => h.includes("anglais"));
905
+ return {
906
+ francais: francaisIdx >= 0 ? rows[0][francaisIdx] ?? null : null,
907
+ anglais: anglaisIdx >= 0 ? rows[0][anglaisIdx] ?? null : null
908
+ };
909
+ } catch (error) {
910
+ this.log("Error getting language levels:", error);
911
+ return null;
912
+ }
913
+ }
914
+ /**
915
+ * Get historical academic ranking data across multiple years.
916
+ *
917
+ * Fetches data from ranking.aspx showing the student's rank, average, and class for each academic year.
918
+ * The table ID can vary, so multiple IDs are tried with a fallback to structure-based detection.
919
+ *
920
+ * @returns Array of RankingEntry objects, or null if unavailable
921
+ */
922
+ async getRanking() {
923
+ try {
924
+ const response = await this.client.get(this.RANKING_URL, { maxRedirects: 5 });
925
+ if (isLoginPage(response.request?.res?.responseUrl ?? "")) {
926
+ this.log("Session expired - redirected to login");
927
+ return null;
928
+ }
929
+ const $ = cheerio2.load(response.data);
930
+ let table = $();
931
+ const possibleIds = [
932
+ "ContentPlaceHolder1_GridView1_5",
933
+ "ContentPlaceHolder1_GridView1_4",
934
+ "ContentPlaceHolder1_GridView1_3",
935
+ "ContentPlaceHolder1_GridView1_2",
936
+ "ContentPlaceHolder1_GridView1"
937
+ ];
938
+ for (const id of possibleIds) {
939
+ table = $(`#${id}`);
940
+ if (table.length > 0) {
941
+ this.log("Found ranking table with ID:", id);
942
+ break;
943
+ }
944
+ }
945
+ if (table.length === 0) {
946
+ this.log("Trying fallback table detection");
947
+ $("table").each((_, el) => {
948
+ const firstRow = $(el).find("tr").first();
949
+ const headers2 = firstRow.find("th").map((_2, th) => $(th).text().trim().toLowerCase()).get();
950
+ if (headers2.some((h) => h.includes("a.u") || h.includes("rang"))) {
951
+ table = $(el);
952
+ this.log("Found ranking table by structure");
953
+ return false;
954
+ }
955
+ });
956
+ }
957
+ if (table.length === 0) {
958
+ this.log("Ranking table not found");
959
+ return null;
960
+ }
961
+ const { headers, rows } = parseTable($, table);
962
+ if (rows.length === 0) {
963
+ this.log("No ranking data found");
964
+ return null;
965
+ }
966
+ const headerLower = headers.map((h) => h.toLowerCase());
967
+ const auIdx = headerLower.findIndex((h) => h.includes("a.u") || h.includes("ann\xE9e"));
968
+ const classeIdx = headerLower.findIndex((h) => h.includes("classe"));
969
+ const moyenneIdx = headerLower.findIndex((h) => h.includes("moyenne"));
970
+ const rangIdx = headerLower.findIndex((h) => h.includes("rang"));
971
+ return rows.map((row) => ({
972
+ anneeUniversitaire: auIdx >= 0 ? row[auIdx] ?? null : null,
973
+ classe: classeIdx >= 0 ? row[classeIdx] ?? null : null,
974
+ moyenne: moyenneIdx >= 0 ? row[moyenneIdx] ?? null : null,
975
+ rang: rangIdx >= 0 ? row[rangIdx] ?? null : null
976
+ }));
977
+ } catch (error) {
978
+ this.log("Error getting ranking:", error);
979
+ return null;
980
+ }
981
+ }
688
982
  };
689
983
  var index_default = EspritClient;
690
984
  // Annotate the CommonJS export names for ESM import in node: