@mrselva/attendance 1.0.0 → 1.1.2
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 +23 -42
- package/index.js +171 -50
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> A fast and lightweight CLI tool to extract, normalize, and export multi-sheet attendance spreadsheets into organized Excel files and ZIP archives.
|
|
8
8
|
|
|
9
|
-
Run instantly with **`npx`** — no
|
|
9
|
+
Run instantly with **`npx`** — no installation required!
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## ⚡ Quick Start
|
|
14
14
|
|
|
15
|
-
Open your terminal in any
|
|
15
|
+
Open your terminal in any directory and run:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npx @mrselva/attendance
|
|
@@ -29,64 +29,51 @@ attendance
|
|
|
29
29
|
|
|
30
30
|
## ✨ Features
|
|
31
31
|
|
|
32
|
-
- **Zero Configuration
|
|
33
|
-
- **Interactive CLI Wizard**: Clean, color-coded terminal prompts to pick
|
|
34
|
-
- **
|
|
35
|
-
- `AD`: AI & Data Science Lab
|
|
36
|
-
- `BW`: Blockchain and Web3.0 Lab
|
|
37
|
-
- `CD`: Cloud and DevOps Lab
|
|
38
|
-
- `CS`: Cyber Security Lab
|
|
39
|
-
- `DMA`: Data & Mobile Analytics Lab
|
|
40
|
-
- `EI`: Embedded & IoT Lab
|
|
41
|
-
- `FW`: Full Stack Web Lab
|
|
42
|
-
- `SOAL`: School of AI Learning
|
|
43
|
-
- `SOP`: School of Programming
|
|
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.
|
|
34
|
+
- **Multi-Sheet Detection**: Automatically discovers all relevant sheets and rosters across the workbook.
|
|
44
35
|
- **Smart Date Filtering**:
|
|
45
|
-
- Export **All recorded dates**
|
|
46
|
-
- Export a **Single date** (e.g. `
|
|
36
|
+
- Export **All recorded dates**
|
|
37
|
+
- Export a **Single date** (e.g. `YYYY-MM-DD`)
|
|
47
38
|
- Export a **Date range** (from `YYYY-MM-DD` to `YYYY-MM-DD`)
|
|
48
39
|
- Export the **Latest date only**
|
|
49
|
-
- **
|
|
50
|
-
|
|
51
|
-
- `Registration Id`
|
|
52
|
-
- `Attendance *` (`PRESENT` or `ABSENT`)
|
|
53
|
-
- **Status Normalization**: Accurately normalizes marks (`P`, `1`, `OD`, `ELITE` → `PRESENT`; `A`, `0`, unrecorded → `ABSENT`).
|
|
54
|
-
- **Automatic ZIP Packaging**: Compresses generated files directly into `./output/<Vertical>_Attendance.zip`.
|
|
40
|
+
- **Status Normalization**: Accurately normalizes attendance status (`PRESENT`, `ABSENT`, etc.) and handles blanks cleanly.
|
|
41
|
+
- **Automatic ZIP Packaging**: Compresses generated export files directly into clean ZIP archives in `./output/`.
|
|
55
42
|
|
|
56
43
|
---
|
|
57
44
|
|
|
58
45
|
## 🚀 CLI Usage & Flags
|
|
59
46
|
|
|
60
|
-
|
|
47
|
+
Run the interactive wizard or pass flags for automation:
|
|
61
48
|
|
|
62
49
|
```bash
|
|
63
50
|
# Interactive mode
|
|
64
51
|
npx @mrselva/attendance
|
|
65
52
|
|
|
66
|
-
# Export
|
|
67
|
-
npx @mrselva/attendance -v
|
|
53
|
+
# Export all dates for a specific sheet
|
|
54
|
+
npx @mrselva/attendance -v SHEET_CODE -a
|
|
68
55
|
|
|
69
56
|
# Export a single date
|
|
70
|
-
npx @mrselva/attendance -v
|
|
57
|
+
npx @mrselva/attendance -v SHEET_CODE -d 2024-09-12
|
|
71
58
|
|
|
72
59
|
# Export the latest date recorded
|
|
73
|
-
npx @mrselva/attendance -v
|
|
60
|
+
npx @mrselva/attendance -v SHEET_CODE --latest
|
|
74
61
|
|
|
75
|
-
# Process ALL
|
|
62
|
+
# Process ALL sheets at once
|
|
76
63
|
npx @mrselva/attendance -v ALL -a
|
|
77
64
|
|
|
78
65
|
# Specify a custom input spreadsheet file
|
|
79
|
-
npx @mrselva/attendance -f "
|
|
66
|
+
npx @mrselva/attendance -f "attendance_sheet.xlsx" -v SHEET_CODE -a
|
|
80
67
|
```
|
|
81
68
|
|
|
82
69
|
### Options Reference
|
|
83
70
|
|
|
84
71
|
| Flag | Long Flag | Description |
|
|
85
72
|
| :--- | :--- | :--- |
|
|
86
|
-
| `-v <code
|
|
87
|
-
| `-a` | `--all` | Export all recorded
|
|
73
|
+
| `-v <code>` | `--vertical <code>` | Sheet code or name (or `ALL`) |
|
|
74
|
+
| `-a` | `--all` | Export all recorded dates |
|
|
88
75
|
| `-d <date>` | `--date <YYYY-MM-DD>` | Export a specific date |
|
|
89
|
-
| | `--latest` | Export the latest
|
|
76
|
+
| | `--latest` | Export the latest recorded date |
|
|
90
77
|
| `-f <file>` | `--file <path>` | Specify input Excel file path or filename |
|
|
91
78
|
| `-h` | `--help` | Display usage and help message |
|
|
92
79
|
|
|
@@ -96,24 +83,18 @@ npx @mrselva/attendance -f "2024-28_SoI_Attendance_Sheet.xlsx" -v FW -a
|
|
|
96
83
|
|
|
97
84
|
```text
|
|
98
85
|
your-folder/
|
|
99
|
-
├── input/ #
|
|
100
|
-
|
|
101
|
-
└── output/ # Automatically generated output
|
|
102
|
-
├── BW_Attendance.zip
|
|
103
|
-
└── CD_Attendance.zip
|
|
86
|
+
├── input/ # Place source spreadsheets here (or auto-import)
|
|
87
|
+
└── output/ # Automatically generated ZIP archives
|
|
104
88
|
```
|
|
105
89
|
|
|
106
|
-
> **Note:** If an `input/` folder is not found, the CLI will look for Excel files directly in your current working directory.
|
|
107
|
-
|
|
108
90
|
---
|
|
109
91
|
|
|
110
92
|
## 🛠 Requirements
|
|
111
93
|
|
|
112
94
|
- **Node.js**: `v16.0.0` or higher
|
|
113
|
-
- **npm**: `v7.0.0` or higher (included with Node.js)
|
|
114
95
|
|
|
115
96
|
---
|
|
116
97
|
|
|
117
98
|
## 📄 License
|
|
118
99
|
|
|
119
|
-
[ISC](LICENSE) © [mrselva
|
|
100
|
+
[ISC](LICENSE) © [mrselva](https://github.com/mrselva)
|
package/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Attendance
|
|
3
|
+
* Attendance Spreadsheet Converter & Exporter
|
|
4
4
|
* Single-file Node.js CLI script
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* - Scans ./input for attendance spreadsheets (.xlsx / .xls)
|
|
8
|
-
* - Interactively prompts for
|
|
7
|
+
* - Scans ./input or current directory for attendance spreadsheets (.xlsx / .xls)
|
|
8
|
+
* - Interactively prompts for sheet/category selection
|
|
9
9
|
* - Allows exporting all recorded dates, specific date, range, or latest date
|
|
10
|
-
* - Generates individual
|
|
11
|
-
* -
|
|
12
|
-
* - Works both interactively (in terminal) and with optional CLI flags (--vertical, --all, etc.)
|
|
10
|
+
* - Generates individual formatted Excel files with ZIP archive in ./output
|
|
11
|
+
* - Works both interactively (in terminal) and with CLI flags (--vertical, --all, etc.)
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
const fs = require('fs');
|
|
16
15
|
const path = require('path');
|
|
17
16
|
const readline = require('readline');
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
18
|
const xlsx = require('xlsx');
|
|
19
19
|
const AdmZip = require('adm-zip');
|
|
20
20
|
|
|
@@ -22,18 +22,8 @@ const AdmZip = require('adm-zip');
|
|
|
22
22
|
const INPUT_DIR = path.resolve(process.cwd(), 'input');
|
|
23
23
|
const OUTPUT_DIR = path.resolve(process.cwd(), 'output');
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const VERTICAL_NAMES = {
|
|
27
|
-
'AD': 'AI & Data Science Lab',
|
|
28
|
-
'BW': 'Blockchain and Web3.0 Lab',
|
|
29
|
-
'CD': 'Cloud and DevOps Lab',
|
|
30
|
-
'CS': 'Cyber Security Lab',
|
|
31
|
-
'DMA': 'Data & Mobile Analytics Lab',
|
|
32
|
-
'EI': 'Embedded & IoT Lab',
|
|
33
|
-
'FW': 'Full Stack Web Lab',
|
|
34
|
-
'SOAL': 'School of AI Learning',
|
|
35
|
-
'SOP': 'School of Programming'
|
|
36
|
-
};
|
|
25
|
+
// Optional category/sheet code to display name mapping
|
|
26
|
+
const VERTICAL_NAMES = {};
|
|
37
27
|
|
|
38
28
|
// ANSI color helpers
|
|
39
29
|
const colors = {
|
|
@@ -200,7 +190,6 @@ function parseVerticalSheet(ws, sheetName) {
|
|
|
200
190
|
let nameColIdx = -1;
|
|
201
191
|
let emailColIdx = -1;
|
|
202
192
|
let rollColIdx = -1;
|
|
203
|
-
let isKiteEmail = false;
|
|
204
193
|
const dateColumns = [];
|
|
205
194
|
|
|
206
195
|
// Analyze header row
|
|
@@ -214,13 +203,8 @@ function parseVerticalSheet(ws, sheetName) {
|
|
|
214
203
|
regColIdx = c;
|
|
215
204
|
} else if (hStr.includes('student name') || hStr === 'name') {
|
|
216
205
|
nameColIdx = c;
|
|
217
|
-
} else if (hStr.includes('
|
|
206
|
+
} else if (hStr.includes('email id') || hStr.includes('email') || hStr.includes('mail')) {
|
|
218
207
|
emailColIdx = c;
|
|
219
|
-
isKiteEmail = true;
|
|
220
|
-
} else if (!isKiteEmail && (hStr.includes('email id') || hStr.includes('email'))) {
|
|
221
|
-
if (!hStr.includes('soi')) {
|
|
222
|
-
emailColIdx = c;
|
|
223
|
-
}
|
|
224
208
|
} else if (hStr.includes('roll number') || hStr.includes('roll no')) {
|
|
225
209
|
rollColIdx = c;
|
|
226
210
|
} else {
|
|
@@ -269,7 +253,7 @@ function parseVerticalSheet(ws, sheetName) {
|
|
|
269
253
|
name: name,
|
|
270
254
|
regNo: regNo,
|
|
271
255
|
rollNo: rollNo,
|
|
272
|
-
email: email ||
|
|
256
|
+
email: email || ''
|
|
273
257
|
});
|
|
274
258
|
}
|
|
275
259
|
|
|
@@ -314,11 +298,11 @@ function parseVerticalSheet(ws, sheetName) {
|
|
|
314
298
|
};
|
|
315
299
|
}
|
|
316
300
|
|
|
317
|
-
// Generate
|
|
318
|
-
function
|
|
301
|
+
// Generate formatted Excel workbook for a single date
|
|
302
|
+
function generateExportWorkbook(verticalData, dateItem, rows) {
|
|
319
303
|
const wb = xlsx.utils.book_new();
|
|
320
304
|
|
|
321
|
-
//
|
|
305
|
+
// Export Sheet Header
|
|
322
306
|
const wsData = [
|
|
323
307
|
['Email Id', 'Registration Id', 'Attendance *']
|
|
324
308
|
];
|
|
@@ -387,6 +371,128 @@ function parseArgs() {
|
|
|
387
371
|
return flags;
|
|
388
372
|
}
|
|
389
373
|
|
|
374
|
+
// Clean user input file path (strip quotes and shell prefixes)
|
|
375
|
+
function cleanPath(input) {
|
|
376
|
+
if (!input) return '';
|
|
377
|
+
let str = input.trim();
|
|
378
|
+
if (str.startsWith('&')) {
|
|
379
|
+
str = str.slice(1).trim();
|
|
380
|
+
}
|
|
381
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
|
|
382
|
+
str = str.slice(1, -1).trim();
|
|
383
|
+
}
|
|
384
|
+
return str;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Open native OS file picker dialog
|
|
388
|
+
function openFileDialog() {
|
|
389
|
+
const platform = process.platform;
|
|
390
|
+
try {
|
|
391
|
+
if (platform === 'win32') {
|
|
392
|
+
const psScript = `
|
|
393
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
394
|
+
$dlg = New-Object System.Windows.Forms.OpenFileDialog
|
|
395
|
+
$dlg.Filter = 'Excel Files (*.xlsx;*.xls)|*.xlsx;*.xls|All Files (*.*)|*.*'
|
|
396
|
+
$dlg.Title = 'Select Attendance Spreadsheet'
|
|
397
|
+
$dlg.RestoreDirectory = $true
|
|
398
|
+
$result = $dlg.ShowDialog()
|
|
399
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
400
|
+
Write-Output $dlg.FileName
|
|
401
|
+
}
|
|
402
|
+
`;
|
|
403
|
+
const output = execFileSync('powershell.exe', ['-NoProfile', '-STA', '-Command', psScript], {
|
|
404
|
+
encoding: 'utf8',
|
|
405
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
406
|
+
timeout: 180000
|
|
407
|
+
});
|
|
408
|
+
return output ? output.trim() : null;
|
|
409
|
+
} else if (platform === 'darwin') {
|
|
410
|
+
const appleScript = 'POSIX path of (choose file with prompt "Select Attendance Spreadsheet" of type {"xlsx", "xls"})';
|
|
411
|
+
const output = execFileSync('osascript', ['-e', appleScript], {
|
|
412
|
+
encoding: 'utf8',
|
|
413
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
414
|
+
});
|
|
415
|
+
return output ? output.trim() : null;
|
|
416
|
+
} else {
|
|
417
|
+
const output = execFileSync('zenity', ['--file-selection', '--file-filter=Excel files | *.xlsx *.xls', '--title=Select Attendance Spreadsheet'], {
|
|
418
|
+
encoding: 'utf8',
|
|
419
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
420
|
+
});
|
|
421
|
+
return output ? output.trim() : null;
|
|
422
|
+
}
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Copy / import a selected spreadsheet into ./input/
|
|
429
|
+
function importFileToInput(srcPath) {
|
|
430
|
+
ensureDirs();
|
|
431
|
+
const fileName = path.basename(srcPath);
|
|
432
|
+
const destPath = path.join(INPUT_DIR, fileName);
|
|
433
|
+
|
|
434
|
+
if (path.resolve(srcPath) !== path.resolve(destPath)) {
|
|
435
|
+
try {
|
|
436
|
+
fs.copyFileSync(srcPath, destPath);
|
|
437
|
+
logSuccess(`Imported "${fileName}" into ./input/ folder!`);
|
|
438
|
+
return {
|
|
439
|
+
name: fileName,
|
|
440
|
+
fullPath: destPath,
|
|
441
|
+
relativePath: path.join('input', fileName)
|
|
442
|
+
};
|
|
443
|
+
} catch (err) {
|
|
444
|
+
logWarn(`Could not copy into ./input/ (${err.message}). Reading from original location.`);
|
|
445
|
+
return {
|
|
446
|
+
name: fileName,
|
|
447
|
+
fullPath: srcPath,
|
|
448
|
+
relativePath: srcPath
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
name: fileName,
|
|
455
|
+
fullPath: destPath,
|
|
456
|
+
relativePath: path.relative(process.cwd(), destPath) || path.join('input', fileName)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Interactive prompt for file selection when needed
|
|
461
|
+
async function promptForFile(lines) {
|
|
462
|
+
logHeader('Select Attendance Spreadsheet');
|
|
463
|
+
console.log('No attendance spreadsheet selected yet.');
|
|
464
|
+
console.log('Please choose how you want to select your Excel file (.xlsx / .xls):\n');
|
|
465
|
+
console.log(` [${colors.bold}1${colors.reset}] ${colors.green}Browse file${colors.reset} (Opens file picker window to click your file) [Default]`);
|
|
466
|
+
console.log(` [${colors.bold}2${colors.reset}] ${colors.cyan}Enter or drag & drop file path here${colors.reset}`);
|
|
467
|
+
|
|
468
|
+
while (true) {
|
|
469
|
+
const choice = (await askQuestion(lines, `\nChoose option [1 or 2] (default: 1): `)).trim();
|
|
470
|
+
|
|
471
|
+
if (choice === '2') {
|
|
472
|
+
const manual = await askQuestion(lines, 'Enter or drag & drop file path: ');
|
|
473
|
+
const cleaned = cleanPath(manual);
|
|
474
|
+
if (cleaned && fs.existsSync(cleaned) && fs.statSync(cleaned).isFile()) {
|
|
475
|
+
const ext = path.extname(cleaned).toLowerCase();
|
|
476
|
+
if (ext === '.xlsx' || ext === '.xls') {
|
|
477
|
+
return cleaned;
|
|
478
|
+
} else {
|
|
479
|
+
logWarn(`"${cleaned}" is not an Excel spreadsheet (.xlsx / .xls).`);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
logError(`File not found at: "${cleaned}". Please check the path and try again.`);
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
console.log(`${colors.dim}Opening file picker dialog... Please select your file in the window.${colors.reset}`);
|
|
486
|
+
const picked = openFileDialog();
|
|
487
|
+
if (picked && fs.existsSync(picked) && fs.statSync(picked).isFile()) {
|
|
488
|
+
return picked;
|
|
489
|
+
}
|
|
490
|
+
logWarn('No file was selected in the dialog.');
|
|
491
|
+
console.log(`You can try browsing again [1], or enter/drag & drop the file path directly [2]:`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
390
496
|
// Interactive prompt helper
|
|
391
497
|
async function askQuestion(lineIterator, promptText) {
|
|
392
498
|
process.stdout.write(promptText);
|
|
@@ -419,7 +525,7 @@ Running without options launches the interactive CLI wizard.
|
|
|
419
525
|
}
|
|
420
526
|
|
|
421
527
|
console.log(`\n${colors.bold}${colors.magenta}╔═════════════════════════════════════════════════════════════════════╗`);
|
|
422
|
-
console.log(`║
|
|
528
|
+
console.log(`║ Attendance Sheet Exporter & Converter ║`);
|
|
423
529
|
console.log(`╚═════════════════════════════════════════════════════════════════════╝${colors.reset}`);
|
|
424
530
|
|
|
425
531
|
ensureDirs();
|
|
@@ -437,38 +543,53 @@ Running without options launches the interactive CLI wizard.
|
|
|
437
543
|
if (flags.file) {
|
|
438
544
|
const directPath = path.resolve(process.cwd(), flags.file);
|
|
439
545
|
const inInputPath = path.join(INPUT_DIR, flags.file);
|
|
546
|
+
let resolvedSrc = null;
|
|
440
547
|
if (fs.existsSync(directPath) && fs.statSync(directPath).isFile()) {
|
|
441
|
-
|
|
548
|
+
resolvedSrc = directPath;
|
|
442
549
|
} else if (fs.existsSync(inInputPath) && fs.statSync(inInputPath).isFile()) {
|
|
443
|
-
|
|
550
|
+
resolvedSrc = inInputPath;
|
|
444
551
|
} else {
|
|
445
552
|
logError(`Specified file "${flags.file}" was not found.`);
|
|
446
553
|
rl.close();
|
|
447
554
|
process.exit(1);
|
|
448
555
|
}
|
|
556
|
+
selectedFileObj = importFileToInput(resolvedSrc);
|
|
449
557
|
} else {
|
|
450
558
|
const inputFiles = findInputFiles();
|
|
451
|
-
if (inputFiles.length === 0) {
|
|
452
|
-
logError(`No Excel spreadsheets (.xlsx / .xls) found in: ${INPUT_DIR} or current directory.`);
|
|
453
|
-
console.log(`\n${colors.yellow}Please place your attendance sheet (e.g. 2024-28_SoI_Attendance_Sheet.xlsx) in the 'input' folder or current directory and run this script again.${colors.reset}\n`);
|
|
454
|
-
rl.close();
|
|
455
|
-
process.exit(1);
|
|
456
|
-
}
|
|
457
559
|
|
|
458
|
-
if (inputFiles.length
|
|
560
|
+
if (inputFiles.length === 0) {
|
|
561
|
+
// Auto prompt to browse or drag & drop when input/ is empty
|
|
562
|
+
const chosen = await promptForFile(lines);
|
|
563
|
+
selectedFileObj = importFileToInput(chosen);
|
|
564
|
+
} else if (inputFiles.length === 1) {
|
|
565
|
+
logInfo(`Found existing attendance spreadsheet: ${colors.bold}${inputFiles[0].relativePath}${colors.reset}`);
|
|
566
|
+
console.log(` [${colors.bold}1${colors.reset}] Use this spreadsheet [Default]`);
|
|
567
|
+
console.log(` [${colors.bold}2${colors.reset}] Choose/import a different file`);
|
|
568
|
+
const pick = (await askQuestion(lines, `\nSelect option [1 or 2] (default: 1): `)).trim();
|
|
569
|
+
if (pick === '2') {
|
|
570
|
+
const chosen = await promptForFile(lines);
|
|
571
|
+
selectedFileObj = importFileToInput(chosen);
|
|
572
|
+
} else {
|
|
573
|
+
selectedFileObj = inputFiles[0];
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
459
576
|
logHeader('Select Input Attendance File');
|
|
460
577
|
inputFiles.forEach((fileObj, idx) => {
|
|
461
578
|
console.log(` [${colors.bold}${idx + 1}${colors.reset}] ${fileObj.relativePath}`);
|
|
462
579
|
});
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
|
|
580
|
+
console.log(` [${colors.bold}N${colors.reset}] ${colors.cyan}Import a new spreadsheet${colors.reset} (Browse or drag & drop)`);
|
|
581
|
+
const ans = (await askQuestion(lines, `\nSelect file [1-${inputFiles.length}] or N (default: 1): `)).trim();
|
|
582
|
+
if (ans.toUpperCase() === 'N') {
|
|
583
|
+
const chosen = await promptForFile(lines);
|
|
584
|
+
selectedFileObj = importFileToInput(chosen);
|
|
467
585
|
} else {
|
|
468
|
-
|
|
586
|
+
const num = parseInt(ans, 10);
|
|
587
|
+
if (!isNaN(num) && num >= 1 && num <= inputFiles.length) {
|
|
588
|
+
selectedFileObj = inputFiles[num - 1];
|
|
589
|
+
} else {
|
|
590
|
+
selectedFileObj = inputFiles[0];
|
|
591
|
+
}
|
|
469
592
|
}
|
|
470
|
-
} else {
|
|
471
|
-
selectedFileObj = inputFiles[0];
|
|
472
593
|
}
|
|
473
594
|
}
|
|
474
595
|
|
|
@@ -610,10 +731,10 @@ Running without options launches the interactive CLI wizard.
|
|
|
610
731
|
rl.close();
|
|
611
732
|
|
|
612
733
|
// Process selected verticals
|
|
613
|
-
logHeader('Processing & Generating
|
|
734
|
+
logHeader('Processing & Generating Export Files');
|
|
614
735
|
|
|
615
736
|
for (const vert of targets) {
|
|
616
|
-
console.log(`\n${colors.bold}
|
|
737
|
+
console.log(`\n${colors.bold}Sheet: ${vert.sheetName}${vert.verticalName !== vert.sheetName ? ' - ' + vert.verticalName : ''}${colors.reset}`);
|
|
617
738
|
console.log(` Students: ${colors.bold}${vert.students.length}${colors.reset}`);
|
|
618
739
|
|
|
619
740
|
let datesToExport = [...vert.dates];
|
|
@@ -651,7 +772,7 @@ Running without options launches the interactive CLI wizard.
|
|
|
651
772
|
|
|
652
773
|
for (const dateItem of datesToExport) {
|
|
653
774
|
const fileName = `Upload_Attendance_${vert.sheetName}_${dateItem.dateStr}.xlsx`;
|
|
654
|
-
const outWb =
|
|
775
|
+
const outWb = generateExportWorkbook(vert, dateItem, rows);
|
|
655
776
|
const fileBuffer = xlsx.write(outWb, { type: 'buffer', bookType: 'xlsx' });
|
|
656
777
|
zip.addFile(fileName, fileBuffer);
|
|
657
778
|
}
|
|
@@ -670,7 +791,7 @@ Running without options launches the interactive CLI wizard.
|
|
|
670
791
|
const zipStat = fs.statSync(zipFilePath);
|
|
671
792
|
const zipSizeKb = (zipStat.size / 1024).toFixed(1);
|
|
672
793
|
|
|
673
|
-
logSuccess(`Generated ZIP archive containing ${datesToExport.length}
|
|
794
|
+
logSuccess(`Generated ZIP archive containing ${datesToExport.length} export files:`);
|
|
674
795
|
console.log(` ${colors.bold}${colors.green}${zipFilePath}${colors.reset} (${zipSizeKb} KB)`);
|
|
675
796
|
}
|
|
676
797
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrselva/attendance",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Extract
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "Extract multi-sheet attendance data and export formatted Excel sheets with ZIP archives",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node index.js"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [
|
|
9
|
+
"keywords": [
|
|
10
|
+
"attendance",
|
|
11
|
+
"excel",
|
|
12
|
+
"spreadsheet",
|
|
13
|
+
"cli",
|
|
14
|
+
"npx"
|
|
15
|
+
],
|
|
10
16
|
"author": "mrselva",
|
|
11
17
|
"license": "ISC",
|
|
12
18
|
"publishConfig": {
|
|
13
19
|
"access": "public"
|
|
14
20
|
},
|
|
15
21
|
"bin": {
|
|
16
|
-
"attendance": "
|
|
22
|
+
"attendance": "index.js"
|
|
17
23
|
},
|
|
18
24
|
"files": [
|
|
19
25
|
"index.js",
|