@mrselva/attendance 1.1.2 → 1.1.3

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.
Files changed (3) hide show
  1. package/README.md +15 -11
  2. package/index.js +274 -105
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,7 +30,9 @@ attendance
30
30
  ## ✨ Features
31
31
 
32
32
  - **Zero Configuration & Auto-Import**: If no file is in `./input/`, an interactive file picker window opens so you can select your spreadsheet directly from your computer, or drag & drop file paths into the terminal.
33
- - **Interactive CLI Wizard**: Clean, color-coded terminal prompts to pick sheets and date filters.
33
+ - **Cohort & Batch Support**:
34
+ - **3rd Year**: Exports standard whole-class roster.
35
+ - **2nd Year**: Automatically detects the empty row separating **Batch 1** (above the empty row) and **Batch 2** (below the empty row). Prompts you to export Batch 1, Batch 2, or Both Batches!
34
36
  - **Multi-Sheet Detection**: Automatically discovers all relevant sheets and rosters across the workbook.
35
37
  - **Smart Date Filtering**:
36
38
  - Export **All recorded dates**
@@ -47,29 +49,31 @@ attendance
47
49
  Run the interactive wizard or pass flags for automation:
48
50
 
49
51
  ```bash
50
- # Interactive mode
52
+ # Interactive mode (asks Year, Batch, Sheet, Dates)
51
53
  npx @mrselva/attendance
52
54
 
53
- # Export all dates for a specific sheet
54
- npx @mrselva/attendance -v SHEET_CODE -a
55
+ # 2nd Year - Export Batch 1
56
+ npx @mrselva/attendance -y 2 -b 1 -v SHEET_CODE -a
55
57
 
56
- # Export a single date
57
- npx @mrselva/attendance -v SHEET_CODE -d 2024-09-12
58
+ # 2nd Year - Export Batch 2
59
+ npx @mrselva/attendance -y 2 -b 2 -v SHEET_CODE -a
58
60
 
59
- # Export the latest date recorded
60
- npx @mrselva/attendance -v SHEET_CODE --latest
61
+ # 2nd Year - Export Both Batches separately
62
+ npx @mrselva/attendance -y 2 -b all -v ALL -a
61
63
 
62
- # Process ALL sheets at once
63
- npx @mrselva/attendance -v ALL -a
64
+ # 3rd Year - Export standard roster
65
+ npx @mrselva/attendance -y 3 -v SHEET_CODE -a
64
66
 
65
67
  # Specify a custom input spreadsheet file
66
- npx @mrselva/attendance -f "attendance_sheet.xlsx" -v SHEET_CODE -a
68
+ npx @mrselva/attendance -f "attendance_sheet.xlsx" -y 2 -b 1 -a
67
69
  ```
68
70
 
69
71
  ### Options Reference
70
72
 
71
73
  | Flag | Long Flag | Description |
72
74
  | :--- | :--- | :--- |
75
+ | `-y <2\|3>` | `--year <2\|3>` | Academic year (`2` for 2nd year batched, `3` for 3rd year standard) |
76
+ | `-b <1\|2\|all>` | `--batch <1\|2\|all>` | Batch selection for 2nd year (`1`, `2`, or `all` for both) |
73
77
  | `-v <code>` | `--vertical <code>` | Sheet code or name (or `ALL`) |
74
78
  | `-a` | `--all` | Export all recorded dates |
75
79
  | `-d <date>` | `--date <YYYY-MM-DD>` | Export a specific date |
package/index.js CHANGED
@@ -179,7 +179,55 @@ function normalizeStatus(val) {
179
179
  return null;
180
180
  }
181
181
 
182
- // Parse a vertical sheet to extract students and date columns
182
+
183
+ // Check if a row represents summary / totals
184
+ function isSummaryRow(row, name, regNo) {
185
+ if (name && (/no\s*\.?\s*of/i.test(name) || /total/i.test(name) || /present/i.test(name) || /absent/i.test(name))) {
186
+ return true;
187
+ }
188
+ if (regNo && (/no\s*\.?\s*of/i.test(regNo) || /total/i.test(regNo) || /present/i.test(regNo) || /absent/i.test(regNo))) {
189
+ return true;
190
+ }
191
+ if (row && Array.isArray(row)) {
192
+ const text = row.map(cell => String(cell || '').trim()).join(' ').toLowerCase();
193
+ return /no\s*\.?\s*of\s*(present|absent)/i.test(text) ||
194
+ /\b(total\s*present|total\s*absent|no\s*\.?\s*of\s*students)\b/i.test(text);
195
+ }
196
+ return false;
197
+ }
198
+
199
+ // Calculate instructional dates for a given student roster
200
+ function getInstructionalDates(students, dateColumns, rows) {
201
+ if (!students || students.length === 0) return [];
202
+ const instructionalDates = [];
203
+ for (const dc of dateColumns) {
204
+ let presentCount = 0;
205
+ let absentCount = 0;
206
+
207
+ for (const student of students) {
208
+ const cellVal = rows[student.rowIdx] ? rows[student.rowIdx][dc.colIdx] : null;
209
+ const norm = normalizeStatus(cellVal);
210
+ if (norm === 'PRESENT') {
211
+ presentCount++;
212
+ } else if (norm === 'ABSENT') {
213
+ absentCount++;
214
+ }
215
+ }
216
+
217
+ if (presentCount + absentCount > 0) {
218
+ instructionalDates.push({
219
+ ...dc,
220
+ presentCount,
221
+ absentCount,
222
+ totalStudents: students.length
223
+ });
224
+ }
225
+ }
226
+ instructionalDates.sort((a, b) => a.dateStr.localeCompare(b.dateStr));
227
+ return instructionalDates;
228
+ }
229
+
230
+ // Parse a vertical sheet to extract students (batch 1 & 2) and date columns
183
231
  function parseVerticalSheet(ws, sheetName) {
184
232
  const rows = xlsx.utils.sheet_to_json(ws, { header: 1, raw: true, defval: null });
185
233
  if (!rows || rows.length < 2) return null;
@@ -222,11 +270,20 @@ function parseVerticalSheet(ws, sheetName) {
222
270
  return null; // Not a vertical attendance sheet
223
271
  }
224
272
 
225
- // Parse student records
226
- const students = [];
273
+ // Parse student records, separating Batch 1 and Batch 2 across empty separator row
274
+ const batch1Students = [];
275
+ const batch2Students = [];
276
+ let currentBatch = 1;
277
+ let sawBlankAfterBatch1 = false;
278
+
227
279
  for (let r = 1; r < rows.length; r++) {
228
280
  const row = rows[r];
229
- if (!row) continue;
281
+ if (!row || row.length === 0) {
282
+ if (batch1Students.length > 0 && currentBatch === 1) {
283
+ sawBlankAfterBatch1 = true;
284
+ }
285
+ continue;
286
+ }
230
287
 
231
288
  const sno = row[0];
232
289
  const name = nameColIdx !== -1 ? String(row[nameColIdx] || '').trim() : '';
@@ -235,71 +292,66 @@ function parseVerticalSheet(ws, sheetName) {
235
292
  const rollNo = rollColIdx !== -1 ? String(row[rollColIdx] || '').trim() : '';
236
293
 
237
294
  // Ignore summary/totals rows at bottom
238
- const isSummary = /no\s*\.?\s*of/i.test(name) ||
239
- /no\s*\.?\s*of/i.test(regNo) ||
240
- /total/i.test(name) ||
241
- /total/i.test(regNo) ||
242
- /present/i.test(regNo) ||
243
- /absent/i.test(regNo);
244
- if (isSummary) continue;
245
-
246
- if (!name || name.length < 2) continue;
247
- if (sno !== null && sno !== undefined && isNaN(Number(sno))) continue;
248
- if (!regNo && !rollNo) continue;
249
-
250
- students.push({
295
+ if (isSummaryRow(row, name, regNo)) {
296
+ break;
297
+ }
298
+
299
+ const isStudent = name.length >= 2 && (regNo.length > 0 || rollNo.length > 0);
300
+
301
+ if (!isStudent) {
302
+ if (batch1Students.length > 0 && currentBatch === 1) {
303
+ sawBlankAfterBatch1 = true;
304
+ }
305
+ continue;
306
+ }
307
+
308
+ // Valid student row
309
+ const snoNum = Number(sno);
310
+ if (batch1Students.length > 0 && currentBatch === 1 && (sawBlankAfterBatch1 || snoNum === 1)) {
311
+ currentBatch = 2;
312
+ }
313
+
314
+ const studentObj = {
251
315
  rowIdx: r,
252
316
  sno: sno,
253
317
  name: name,
254
318
  regNo: regNo,
255
319
  rollNo: rollNo,
256
- email: email || ''
257
- });
258
- }
259
-
260
- if (students.length === 0) return null;
261
-
262
- // Filter dates to only those that have actual student attendance recorded
263
- const instructionalDates = [];
264
- for (const dc of dateColumns) {
265
- let presentCount = 0;
266
- let absentCount = 0;
320
+ email: email || '',
321
+ batch: currentBatch
322
+ };
267
323
 
268
- for (const student of students) {
269
- const cellVal = rows[student.rowIdx] ? rows[student.rowIdx][dc.colIdx] : null;
270
- const norm = normalizeStatus(cellVal);
271
- if (norm === 'PRESENT') {
272
- presentCount++;
273
- } else if (norm === 'ABSENT') {
274
- absentCount++;
275
- }
276
- }
277
-
278
- // Only if at least one student was marked PRESENT or ABSENT
279
- if (presentCount + absentCount > 0) {
280
- instructionalDates.push({
281
- ...dc,
282
- presentCount,
283
- absentCount,
284
- totalStudents: students.length
285
- });
324
+ if (currentBatch === 1) {
325
+ batch1Students.push(studentObj);
326
+ } else {
327
+ batch2Students.push(studentObj);
286
328
  }
287
329
  }
288
330
 
289
- // Sort dates chronologically
290
- instructionalDates.sort((a, b) => a.dateStr.localeCompare(b.dateStr));
331
+ const allStudents = [...batch1Students, ...batch2Students];
332
+ if (allStudents.length === 0) return null;
333
+
334
+ const allDates = getInstructionalDates(allStudents, dateColumns, rows);
335
+ const batch1Dates = getInstructionalDates(batch1Students, dateColumns, rows);
336
+ const batch2Dates = getInstructionalDates(batch2Students, dateColumns, rows);
291
337
 
292
338
  return {
293
339
  sheetName,
294
340
  verticalName: VERTICAL_NAMES[sheetName] || sheetName,
295
- students,
296
- dates: instructionalDates,
297
- allDateColumns: dateColumns
341
+ students: allStudents,
342
+ batch1Students,
343
+ batch2Students,
344
+ hasBatches: batch1Students.length > 0 && batch2Students.length > 0,
345
+ dates: allDates,
346
+ batch1Dates,
347
+ batch2Dates,
348
+ allDateColumns: dateColumns,
349
+ rows
298
350
  };
299
351
  }
300
352
 
301
353
  // Generate formatted Excel workbook for a single date
302
- function generateExportWorkbook(verticalData, dateItem, rows) {
354
+ function generateExportWorkbook(verticalData, dateItem, rows, studentList) {
303
355
  const wb = xlsx.utils.book_new();
304
356
 
305
357
  // Export Sheet Header
@@ -307,7 +359,9 @@ function generateExportWorkbook(verticalData, dateItem, rows) {
307
359
  ['Email Id', 'Registration Id', 'Attendance *']
308
360
  ];
309
361
 
310
- for (const student of verticalData.students) {
362
+ const students = studentList || verticalData.students;
363
+
364
+ for (const student of students) {
311
365
  const rawVal = rows[student.rowIdx] ? rows[student.rowIdx][dateItem.colIdx] : null;
312
366
  let status = normalizeStatus(rawVal);
313
367
  // On an instructional day, any unrecorded or blank student is marked ABSENT
@@ -346,6 +400,8 @@ function parseArgs() {
346
400
  endDate: null,
347
401
  latest: false,
348
402
  file: null,
403
+ year: null,
404
+ batch: null,
349
405
  help: false
350
406
  };
351
407
 
@@ -355,6 +411,10 @@ function parseArgs() {
355
411
  flags.help = true;
356
412
  } else if (a === '-v' || a === '--vertical') {
357
413
  flags.vertical = args[++i];
414
+ } else if (a === '-y' || a === '--year') {
415
+ flags.year = args[++i];
416
+ } else if (a === '-b' || a === '--batch') {
417
+ flags.batch = args[++i];
358
418
  } else if (a === '-a' || a === '--all' || a === '--all-dates') {
359
419
  flags.all = true;
360
420
  } else if (a === '-d' || a === '--date') {
@@ -512,7 +572,9 @@ Usage: npx @mrselva/attendance [options] [vertical]
512
572
  or: node index.js [options] [vertical]
513
573
 
514
574
  Options:
515
- -v, --vertical <code/name> Vertical code (e.g., AD, BW, CD, CS, DMA, EI, FW, ALL)
575
+ -y, --year <2|3> Academic year (2 for 2nd year batched, 3 for 3rd year standard)
576
+ -b, --batch <1|2|all> Batch selection for 2nd year (1, 2, or all)
577
+ -v, --vertical <code/name> Sheet/vertical code (e.g., AD, BW, CD, CS, DMA, EI, FW, ALL)
516
578
  -a, --all Export all instructional dates automatically
517
579
  -d, --date <YYYY-MM-DD> Export a single specific date
518
580
  --latest Export the latest instructional date
@@ -626,6 +688,63 @@ Running without options launches the interactive CLI wizard.
626
688
  process.exit(1);
627
689
  }
628
690
 
691
+ // Academic Year Selection (2nd Year vs 3rd Year)
692
+ let selectedYear = null;
693
+ if (flags.year) {
694
+ const yStr = String(flags.year).trim();
695
+ if (yStr.startsWith('2')) selectedYear = 2;
696
+ else if (yStr.startsWith('3')) selectedYear = 3;
697
+ }
698
+
699
+ if (!selectedYear) {
700
+ logHeader('Select Academic Year');
701
+ console.log('Choose student cohort / year:\n');
702
+ console.log(` [${colors.bold}1${colors.reset}] ${colors.green}3rd Year${colors.reset} (Standard whole class / single list) [Default]`);
703
+ console.log(` [${colors.bold}2${colors.reset}] ${colors.cyan}2nd Year${colors.reset} (Batched: Batch 1 & Batch 2 separated by empty row)`);
704
+
705
+ const yearAns = (await askQuestion(lines, `\nSelect year [1 or 2] (default: 1): `)).trim();
706
+ if (yearAns === '2' || yearAns.toLowerCase().startsWith('2')) {
707
+ selectedYear = 2;
708
+ } else {
709
+ selectedYear = 3;
710
+ }
711
+ }
712
+
713
+ // Batch selection for 2nd Year
714
+ let selectedBatch = null; // 1, 2, or 'BOTH'
715
+ if (selectedYear === 2) {
716
+ if (flags.batch) {
717
+ const bStr = String(flags.batch).trim().toLowerCase();
718
+ if (bStr === '1' || bStr === 'b1' || bStr === 'batch1' || bStr === '1st') selectedBatch = 1;
719
+ else if (bStr === '2' || bStr === 'b2' || bStr === 'batch2' || bStr === '2nd') selectedBatch = 2;
720
+ else if (bStr === 'all' || bStr === 'both' || bStr === '3') selectedBatch = 'BOTH';
721
+ }
722
+
723
+ if (!selectedBatch) {
724
+ logHeader('Select Batch for 2nd Year');
725
+ console.log('Choose which batch to export:\n');
726
+ console.log(` [${colors.bold}1${colors.reset}] ${colors.green}Batch 1${colors.reset} (Students above the empty row) [Default]`);
727
+ console.log(` [${colors.bold}2${colors.reset}] ${colors.cyan}Batch 2${colors.reset} (Students below the empty row)`);
728
+ console.log(` [${colors.bold}3${colors.reset}] ${colors.yellow}Both Batches${colors.reset} (Export Batch 1 and Batch 2 separately)`);
729
+
730
+ const batchAns = (await askQuestion(lines, `\nSelect batch [1-3] (default: 1): `)).trim();
731
+ if (batchAns === '2' || batchAns.toLowerCase().startsWith('2')) {
732
+ selectedBatch = 2;
733
+ } else if (batchAns === '3' || batchAns.toLowerCase().startsWith('3') || batchAns.toLowerCase() === 'both' || batchAns.toLowerCase() === 'all') {
734
+ selectedBatch = 'BOTH';
735
+ } else {
736
+ selectedBatch = 1;
737
+ }
738
+ }
739
+ }
740
+
741
+ if (selectedYear === 2) {
742
+ const batchLabelStr = selectedBatch === 'BOTH' ? 'Both Batches (Batch 1 & Batch 2)' : `Batch ${selectedBatch}`;
743
+ logInfo(`Cohort: ${colors.bold}2nd Year${colors.reset} | Target: ${colors.bold}${batchLabelStr}${colors.reset}`);
744
+ } else {
745
+ logInfo(`Cohort: ${colors.bold}3rd Year (Standard Whole Class)${colors.reset}`);
746
+ }
747
+
629
748
  // Vertical Selection
630
749
  let chosenVertical = null;
631
750
  let processAll = false;
@@ -653,7 +772,18 @@ Running without options launches the interactive CLI wizard.
653
772
  verticalSheets.forEach((v, idx) => {
654
773
  const code = colors.bold + v.sheetName.padEnd(5) + colors.reset;
655
774
  const name = (v.verticalName !== v.sheetName ? `(${v.verticalName})` : '').padEnd(35);
656
- const countInfo = `${colors.dim}${v.students.length} students, ${v.dates.length} dates recorded${colors.reset}`;
775
+ let countInfo = '';
776
+ if (selectedYear === 2) {
777
+ if (selectedBatch === 1) {
778
+ countInfo = `${colors.dim}${v.batch1Students.length} students (Batch 1), ${v.batch1Dates.length} dates${colors.reset}`;
779
+ } else if (selectedBatch === 2) {
780
+ countInfo = `${colors.dim}${v.batch2Students.length} students (Batch 2), ${v.batch2Dates.length} dates${colors.reset}`;
781
+ } else {
782
+ countInfo = `${colors.dim}B1: ${v.batch1Students.length} sts, B2: ${v.batch2Students.length} sts${colors.reset}`;
783
+ }
784
+ } else {
785
+ countInfo = `${colors.dim}${v.students.length} students, ${v.dates.length} dates recorded${colors.reset}`;
786
+ }
657
787
  console.log(` [${colors.bold}${idx + 1}${colors.reset}] ${code} ${name} ${countInfo}`);
658
788
  });
659
789
  console.log(` [${colors.bold}A${colors.reset}] ${colors.bold}ALL ${colors.reset} (Process ALL verticals at once)`);
@@ -687,6 +817,16 @@ Running without options launches the interactive CLI wizard.
687
817
 
688
818
  const targets = processAll ? verticalSheets : [chosenVertical];
689
819
 
820
+ // Helper to get dates for chosen cohort & batch
821
+ function getDatesForVertical(vert) {
822
+ if (selectedYear === 2) {
823
+ if (selectedBatch === 1) return vert.batch1Dates;
824
+ if (selectedBatch === 2) return vert.batch2Dates;
825
+ return vert.dates;
826
+ }
827
+ return vert.dates;
828
+ }
829
+
690
830
  // Date selection logic
691
831
  let dateFilterMode = 'ALL';
692
832
  let singleDateVal = flags.date || null;
@@ -699,11 +839,13 @@ Running without options launches the interactive CLI wizard.
699
839
  } else if (flags.date) {
700
840
  dateFilterMode = 'SINGLE';
701
841
  } else if (!processAll) {
702
- const totalDates = chosenVertical.dates.length;
703
- const firstDate = totalDates > 0 ? chosenVertical.dates[0].dateStr : 'N/A';
704
- const lastDate = totalDates > 0 ? chosenVertical.dates[totalDates - 1].dateStr : 'N/A';
842
+ const refDates = getDatesForVertical(chosenVertical);
843
+ const totalDates = refDates.length;
844
+ const firstDate = totalDates > 0 ? refDates[0].dateStr : 'N/A';
845
+ const lastDate = totalDates > 0 ? refDates[totalDates - 1].dateStr : 'N/A';
705
846
 
706
- logHeader(`Date Selection for Vertical: ${chosenVertical.sheetName}`);
847
+ const batchNameHeader = selectedYear === 2 ? (selectedBatch === 'BOTH' ? ' (Batches 1 & 2)' : ` (Batch ${selectedBatch})`) : '';
848
+ logHeader(`Date Selection for Sheet: ${chosenVertical.sheetName}${batchNameHeader}`);
707
849
  console.log(`Found ${colors.bold}${totalDates}${colors.reset} instructional dates (${firstDate} to ${lastDate}).\n`);
708
850
  console.log(` [${colors.bold}1${colors.reset}] Export ${colors.green}ALL ${totalDates} dates${colors.reset} (Recommended - generates upload files for each date)`);
709
851
  console.log(` [${colors.bold}2${colors.reset}] Export a ${colors.cyan}single specific date${colors.reset} (e.g., ${lastDate})`);
@@ -734,65 +876,92 @@ Running without options launches the interactive CLI wizard.
734
876
  logHeader('Processing & Generating Export Files');
735
877
 
736
878
  for (const vert of targets) {
737
- console.log(`\n${colors.bold}Sheet: ${vert.sheetName}${vert.verticalName !== vert.sheetName ? ' - ' + vert.verticalName : ''}${colors.reset}`);
738
- console.log(` Students: ${colors.bold}${vert.students.length}${colors.reset}`);
879
+ let batchesToProcess = [];
880
+ if (selectedYear === 2) {
881
+ if (selectedBatch === 1) {
882
+ batchesToProcess = [{ label: 'Batch1', students: vert.batch1Students, dates: vert.batch1Dates }];
883
+ } else if (selectedBatch === 2) {
884
+ batchesToProcess = [{ label: 'Batch2', students: vert.batch2Students, dates: vert.batch2Dates }];
885
+ } else {
886
+ // BOTH
887
+ batchesToProcess = [
888
+ { label: 'Batch1', students: vert.batch1Students, dates: vert.batch1Dates },
889
+ { label: 'Batch2', students: vert.batch2Students, dates: vert.batch2Dates }
890
+ ];
891
+ }
892
+ } else {
893
+ // 3rd Year
894
+ batchesToProcess = [{ label: '', students: vert.students, dates: vert.dates }];
895
+ }
739
896
 
740
- let datesToExport = [...vert.dates];
897
+ for (const batchItem of batchesToProcess) {
898
+ const batchTag = batchItem.label ? ` (${batchItem.label})` : '';
899
+ console.log(`\n${colors.bold}Sheet: ${vert.sheetName}${vert.verticalName !== vert.sheetName ? ' - ' + vert.verticalName : ''}${batchTag}${colors.reset}`);
900
+ console.log(` Students: ${colors.bold}${batchItem.students.length}${colors.reset}`);
741
901
 
742
- if (dateFilterMode === 'SINGLE' && singleDateVal) {
743
- datesToExport = datesToExport.filter(d => d.dateStr === singleDateVal);
744
- if (datesToExport.length === 0) {
745
- logWarn(`Date ${singleDateVal} was not found among instructional dates for ${vert.sheetName}.`);
746
- const fallbackCol = vert.allDateColumns.find(d => d.dateStr === singleDateVal);
747
- if (fallbackCol) {
748
- datesToExport = [{ ...fallbackCol, presentCount: 0, absentCount: 0, totalStudents: vert.students.length }];
749
- } else {
750
- logError(`Date ${singleDateVal} does not exist in the spreadsheet!`);
751
- continue;
902
+ if (batchItem.students.length === 0) {
903
+ logWarn(`No students found for ${vert.sheetName}${batchTag}. Skipping.`);
904
+ continue;
905
+ }
906
+
907
+ let datesToExport = [...batchItem.dates];
908
+
909
+ if (dateFilterMode === 'SINGLE' && singleDateVal) {
910
+ datesToExport = datesToExport.filter(d => d.dateStr === singleDateVal);
911
+ if (datesToExport.length === 0) {
912
+ logWarn(`Date ${singleDateVal} was not found among instructional dates for ${vert.sheetName}${batchTag}.`);
913
+ const fallbackCol = vert.allDateColumns.find(d => d.dateStr === singleDateVal);
914
+ if (fallbackCol) {
915
+ datesToExport = [{ ...fallbackCol, presentCount: 0, absentCount: 0, totalStudents: batchItem.students.length }];
916
+ } else {
917
+ logError(`Date ${singleDateVal} does not exist in the spreadsheet!`);
918
+ continue;
919
+ }
752
920
  }
921
+ } else if (dateFilterMode === 'RANGE' && dateRangeVal) {
922
+ datesToExport = datesToExport.filter(d => d.dateStr >= dateRangeVal.start && d.dateStr <= dateRangeVal.end);
923
+ } else if (dateFilterMode === 'LATEST' && datesToExport.length > 0) {
924
+ datesToExport = [datesToExport[datesToExport.length - 1]];
753
925
  }
754
- } else if (dateFilterMode === 'RANGE' && dateRangeVal) {
755
- datesToExport = datesToExport.filter(d => d.dateStr >= dateRangeVal.start && d.dateStr <= dateRangeVal.end);
756
- } else if (dateFilterMode === 'LATEST' && datesToExport.length > 0) {
757
- datesToExport = [datesToExport[datesToExport.length - 1]];
758
- }
759
926
 
760
- if (datesToExport.length === 0) {
761
- logWarn(`No matching dates found to export for ${vert.sheetName}.`);
762
- continue;
763
- }
927
+ if (datesToExport.length === 0) {
928
+ logWarn(`No matching dates found to export for ${vert.sheetName}${batchTag}.`);
929
+ continue;
930
+ }
764
931
 
765
- console.log(` Dates to export: ${colors.bold}${datesToExport.length}${colors.reset}`);
932
+ console.log(` Dates to export: ${colors.bold}${datesToExport.length}${colors.reset}`);
766
933
 
767
- const ws = workbook.Sheets[vert.sheetName];
768
- const rows = xlsx.utils.sheet_to_json(ws, { header: 1, raw: true, defval: null });
934
+ const rows = vert.rows || xlsx.utils.sheet_to_json(workbook.Sheets[vert.sheetName], { header: 1, raw: true, defval: null });
935
+ const zip = new AdmZip();
769
936
 
770
- // Build ZIP archive directly in memory
771
- const zip = new AdmZip();
937
+ const yearTag = selectedYear === 2 ? '2nd_Year' : '3rd_Year';
772
938
 
773
- for (const dateItem of datesToExport) {
774
- const fileName = `Upload_Attendance_${vert.sheetName}_${dateItem.dateStr}.xlsx`;
775
- const outWb = generateExportWorkbook(vert, dateItem, rows);
776
- const fileBuffer = xlsx.write(outWb, { type: 'buffer', bookType: 'xlsx' });
777
- zip.addFile(fileName, fileBuffer);
778
- }
939
+ for (const dateItem of datesToExport) {
940
+ const batchFileSuffix = batchItem.label ? `_${batchItem.label}` : '';
941
+ const fileName = `${dateItem.dateStr}_Upload_Attendance_${vert.sheetName}${batchFileSuffix}.xlsx`;
942
+ const outWb = generateExportWorkbook(vert, dateItem, rows, batchItem.students);
943
+ const fileBuffer = xlsx.write(outWb, { type: 'buffer', bookType: 'xlsx' });
944
+ zip.addFile(fileName, fileBuffer);
945
+ }
779
946
 
780
- // Save only the ZIP archive in output/
781
- const zipFileName = `${vert.sheetName}_Attendance.zip`;
782
- const zipFilePath = path.join(OUTPUT_DIR, zipFileName);
783
- zip.writeZip(zipFilePath);
947
+ const batchZipSuffix = batchItem.label ? `_${batchItem.label}` : '';
948
+ const zipBaseName = `${yearTag}_${vert.sheetName}${batchZipSuffix}_Attendance`;
949
+ const zipFileName = `${zipBaseName}.zip`;
950
+ const zipFilePath = path.join(OUTPUT_DIR, zipFileName);
951
+ zip.writeZip(zipFilePath);
784
952
 
785
- // Clean up any previously created unzipped folder for this vertical if it exists
786
- const oldVertFolder = path.join(OUTPUT_DIR, `${vert.sheetName}_Attendance`);
787
- if (fs.existsSync(oldVertFolder) && fs.statSync(oldVertFolder).isDirectory()) {
788
- fs.rmSync(oldVertFolder, { recursive: true, force: true });
789
- }
953
+ // Clean up any previously created unzipped folder for this vertical if it exists
954
+ const oldVertFolder = path.join(OUTPUT_DIR, zipBaseName);
955
+ if (fs.existsSync(oldVertFolder) && fs.statSync(oldVertFolder).isDirectory()) {
956
+ fs.rmSync(oldVertFolder, { recursive: true, force: true });
957
+ }
790
958
 
791
- const zipStat = fs.statSync(zipFilePath);
792
- const zipSizeKb = (zipStat.size / 1024).toFixed(1);
959
+ const zipStat = fs.statSync(zipFilePath);
960
+ const zipSizeKb = (zipStat.size / 1024).toFixed(1);
793
961
 
794
- logSuccess(`Generated ZIP archive containing ${datesToExport.length} export files:`);
795
- console.log(` ${colors.bold}${colors.green}${zipFilePath}${colors.reset} (${zipSizeKb} KB)`);
962
+ logSuccess(`Generated ZIP archive containing ${datesToExport.length} export files:`);
963
+ console.log(` ${colors.bold}${colors.green}${zipFilePath}${colors.reset} (${zipSizeKb} KB)`);
964
+ }
796
965
  }
797
966
 
798
967
  // Final summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrselva/attendance",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Extract multi-sheet attendance data and export formatted Excel sheets with ZIP archives",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -33,4 +33,4 @@
33
33
  "adm-zip": "^0.6.1",
34
34
  "xlsx": "^0.18.5"
35
35
  }
36
- }
36
+ }