@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.
- package/README.md +15 -11
- package/index.js +274 -105
- 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
|
-
- **
|
|
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
|
-
#
|
|
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
|
|
57
|
-
npx @mrselva/attendance -
|
|
58
|
+
# 2nd Year - Export Batch 2
|
|
59
|
+
npx @mrselva/attendance -y 2 -b 2 -v SHEET_CODE -a
|
|
58
60
|
|
|
59
|
-
# Export
|
|
60
|
-
npx @mrselva/attendance -v
|
|
61
|
+
# 2nd Year - Export Both Batches separately
|
|
62
|
+
npx @mrselva/attendance -y 2 -b all -v ALL -a
|
|
61
63
|
|
|
62
|
-
#
|
|
63
|
-
npx @mrselva/attendance -v
|
|
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" -
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
703
|
-
const
|
|
704
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
927
|
+
if (datesToExport.length === 0) {
|
|
928
|
+
logWarn(`No matching dates found to export for ${vert.sheetName}${batchTag}.`);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
764
931
|
|
|
765
|
-
|
|
932
|
+
console.log(` Dates to export: ${colors.bold}${datesToExport.length}${colors.reset}`);
|
|
766
933
|
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
771
|
-
const zip = new AdmZip();
|
|
937
|
+
const yearTag = selectedYear === 2 ? '2nd_Year' : '3rd_Year';
|
|
772
938
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
792
|
-
|
|
959
|
+
const zipStat = fs.statSync(zipFilePath);
|
|
960
|
+
const zipSizeKb = (zipStat.size / 1024).toFixed(1);
|
|
793
961
|
|
|
794
|
-
|
|
795
|
-
|
|
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.
|
|
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
|
+
}
|