@mrselva/attendance 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 +1 -1
- package/index.js +154 -16
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ attendance
|
|
|
29
29
|
|
|
30
30
|
## ✨ Features
|
|
31
31
|
|
|
32
|
-
- **Zero Configuration
|
|
32
|
+
- **Zero Configuration & Auto-Import**: If no file is in `./input/`, an OS file picker dialog pops up so you can simply click your spreadsheet (from Downloads or anywhere), and it automatically imports it into `./input/`! Drag & drop file paths into the terminal is also supported.
|
|
33
33
|
- **Interactive CLI Wizard**: Clean, color-coded terminal prompts to pick verticals and date ranges.
|
|
34
34
|
- **Comprehensive Vertical Support**: Detects lab vertical sheets:
|
|
35
35
|
- `AD`: AI & Data Science Lab
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const readline = require('readline');
|
|
18
|
+
const { execFileSync } = require('child_process');
|
|
18
19
|
const xlsx = require('xlsx');
|
|
19
20
|
const AdmZip = require('adm-zip');
|
|
20
21
|
|
|
@@ -387,6 +388,128 @@ function parseArgs() {
|
|
|
387
388
|
return flags;
|
|
388
389
|
}
|
|
389
390
|
|
|
391
|
+
// Clean user input file path (strip quotes and shell prefixes)
|
|
392
|
+
function cleanPath(input) {
|
|
393
|
+
if (!input) return '';
|
|
394
|
+
let str = input.trim();
|
|
395
|
+
if (str.startsWith('&')) {
|
|
396
|
+
str = str.slice(1).trim();
|
|
397
|
+
}
|
|
398
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
|
|
399
|
+
str = str.slice(1, -1).trim();
|
|
400
|
+
}
|
|
401
|
+
return str;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Open native OS file picker dialog
|
|
405
|
+
function openFileDialog() {
|
|
406
|
+
const platform = process.platform;
|
|
407
|
+
try {
|
|
408
|
+
if (platform === 'win32') {
|
|
409
|
+
const psScript = `
|
|
410
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
411
|
+
$dlg = New-Object System.Windows.Forms.OpenFileDialog
|
|
412
|
+
$dlg.Filter = 'Excel Files (*.xlsx;*.xls)|*.xlsx;*.xls|All Files (*.*)|*.*'
|
|
413
|
+
$dlg.Title = 'Select Attendance Spreadsheet'
|
|
414
|
+
$dlg.RestoreDirectory = $true
|
|
415
|
+
$result = $dlg.ShowDialog()
|
|
416
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
417
|
+
Write-Output $dlg.FileName
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
const output = execFileSync('powershell.exe', ['-NoProfile', '-STA', '-Command', psScript], {
|
|
421
|
+
encoding: 'utf8',
|
|
422
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
423
|
+
timeout: 180000
|
|
424
|
+
});
|
|
425
|
+
return output ? output.trim() : null;
|
|
426
|
+
} else if (platform === 'darwin') {
|
|
427
|
+
const appleScript = 'POSIX path of (choose file with prompt "Select Attendance Spreadsheet" of type {"xlsx", "xls"})';
|
|
428
|
+
const output = execFileSync('osascript', ['-e', appleScript], {
|
|
429
|
+
encoding: 'utf8',
|
|
430
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
431
|
+
});
|
|
432
|
+
return output ? output.trim() : null;
|
|
433
|
+
} else {
|
|
434
|
+
const output = execFileSync('zenity', ['--file-selection', '--file-filter=Excel files | *.xlsx *.xls', '--title=Select Attendance Spreadsheet'], {
|
|
435
|
+
encoding: 'utf8',
|
|
436
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
437
|
+
});
|
|
438
|
+
return output ? output.trim() : null;
|
|
439
|
+
}
|
|
440
|
+
} catch (e) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Copy / import a selected spreadsheet into ./input/
|
|
446
|
+
function importFileToInput(srcPath) {
|
|
447
|
+
ensureDirs();
|
|
448
|
+
const fileName = path.basename(srcPath);
|
|
449
|
+
const destPath = path.join(INPUT_DIR, fileName);
|
|
450
|
+
|
|
451
|
+
if (path.resolve(srcPath) !== path.resolve(destPath)) {
|
|
452
|
+
try {
|
|
453
|
+
fs.copyFileSync(srcPath, destPath);
|
|
454
|
+
logSuccess(`Imported "${fileName}" into ./input/ folder!`);
|
|
455
|
+
return {
|
|
456
|
+
name: fileName,
|
|
457
|
+
fullPath: destPath,
|
|
458
|
+
relativePath: path.join('input', fileName)
|
|
459
|
+
};
|
|
460
|
+
} catch (err) {
|
|
461
|
+
logWarn(`Could not copy into ./input/ (${err.message}). Reading from original location.`);
|
|
462
|
+
return {
|
|
463
|
+
name: fileName,
|
|
464
|
+
fullPath: srcPath,
|
|
465
|
+
relativePath: srcPath
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
name: fileName,
|
|
472
|
+
fullPath: destPath,
|
|
473
|
+
relativePath: path.relative(process.cwd(), destPath) || path.join('input', fileName)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Interactive prompt for file selection when needed
|
|
478
|
+
async function promptForFile(lines) {
|
|
479
|
+
logHeader('Select Attendance Spreadsheet');
|
|
480
|
+
console.log('No attendance spreadsheet selected yet.');
|
|
481
|
+
console.log('Please choose how you want to select your Excel file (.xlsx / .xls):\n');
|
|
482
|
+
console.log(` [${colors.bold}1${colors.reset}] ${colors.green}Browse file${colors.reset} (Opens file picker window to click your file) [Default]`);
|
|
483
|
+
console.log(` [${colors.bold}2${colors.reset}] ${colors.cyan}Enter or drag & drop file path here${colors.reset}`);
|
|
484
|
+
|
|
485
|
+
while (true) {
|
|
486
|
+
const choice = (await askQuestion(lines, `\nChoose option [1 or 2] (default: 1): `)).trim();
|
|
487
|
+
|
|
488
|
+
if (choice === '2') {
|
|
489
|
+
const manual = await askQuestion(lines, 'Enter or drag & drop file path: ');
|
|
490
|
+
const cleaned = cleanPath(manual);
|
|
491
|
+
if (cleaned && fs.existsSync(cleaned) && fs.statSync(cleaned).isFile()) {
|
|
492
|
+
const ext = path.extname(cleaned).toLowerCase();
|
|
493
|
+
if (ext === '.xlsx' || ext === '.xls') {
|
|
494
|
+
return cleaned;
|
|
495
|
+
} else {
|
|
496
|
+
logWarn(`"${cleaned}" is not an Excel spreadsheet (.xlsx / .xls).`);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
logError(`File not found at: "${cleaned}". Please check the path and try again.`);
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
console.log(`${colors.dim}Opening file picker dialog... Please select your file in the window.${colors.reset}`);
|
|
503
|
+
const picked = openFileDialog();
|
|
504
|
+
if (picked && fs.existsSync(picked) && fs.statSync(picked).isFile()) {
|
|
505
|
+
return picked;
|
|
506
|
+
}
|
|
507
|
+
logWarn('No file was selected in the dialog.');
|
|
508
|
+
console.log(`You can try browsing again [1], or enter/drag & drop the file path directly [2]:`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
390
513
|
// Interactive prompt helper
|
|
391
514
|
async function askQuestion(lineIterator, promptText) {
|
|
392
515
|
process.stdout.write(promptText);
|
|
@@ -437,38 +560,53 @@ Running without options launches the interactive CLI wizard.
|
|
|
437
560
|
if (flags.file) {
|
|
438
561
|
const directPath = path.resolve(process.cwd(), flags.file);
|
|
439
562
|
const inInputPath = path.join(INPUT_DIR, flags.file);
|
|
563
|
+
let resolvedSrc = null;
|
|
440
564
|
if (fs.existsSync(directPath) && fs.statSync(directPath).isFile()) {
|
|
441
|
-
|
|
565
|
+
resolvedSrc = directPath;
|
|
442
566
|
} else if (fs.existsSync(inInputPath) && fs.statSync(inInputPath).isFile()) {
|
|
443
|
-
|
|
567
|
+
resolvedSrc = inInputPath;
|
|
444
568
|
} else {
|
|
445
569
|
logError(`Specified file "${flags.file}" was not found.`);
|
|
446
570
|
rl.close();
|
|
447
571
|
process.exit(1);
|
|
448
572
|
}
|
|
573
|
+
selectedFileObj = importFileToInput(resolvedSrc);
|
|
449
574
|
} else {
|
|
450
575
|
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
576
|
|
|
458
|
-
if (inputFiles.length
|
|
577
|
+
if (inputFiles.length === 0) {
|
|
578
|
+
// Auto prompt to browse or drag & drop when input/ is empty
|
|
579
|
+
const chosen = await promptForFile(lines);
|
|
580
|
+
selectedFileObj = importFileToInput(chosen);
|
|
581
|
+
} else if (inputFiles.length === 1) {
|
|
582
|
+
logInfo(`Found existing attendance spreadsheet: ${colors.bold}${inputFiles[0].relativePath}${colors.reset}`);
|
|
583
|
+
console.log(` [${colors.bold}1${colors.reset}] Use this spreadsheet [Default]`);
|
|
584
|
+
console.log(` [${colors.bold}2${colors.reset}] Choose/import a different file`);
|
|
585
|
+
const pick = (await askQuestion(lines, `\nSelect option [1 or 2] (default: 1): `)).trim();
|
|
586
|
+
if (pick === '2') {
|
|
587
|
+
const chosen = await promptForFile(lines);
|
|
588
|
+
selectedFileObj = importFileToInput(chosen);
|
|
589
|
+
} else {
|
|
590
|
+
selectedFileObj = inputFiles[0];
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
459
593
|
logHeader('Select Input Attendance File');
|
|
460
594
|
inputFiles.forEach((fileObj, idx) => {
|
|
461
595
|
console.log(` [${colors.bold}${idx + 1}${colors.reset}] ${fileObj.relativePath}`);
|
|
462
596
|
});
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
|
|
597
|
+
console.log(` [${colors.bold}N${colors.reset}] ${colors.cyan}Import a new spreadsheet${colors.reset} (Browse or drag & drop)`);
|
|
598
|
+
const ans = (await askQuestion(lines, `\nSelect file [1-${inputFiles.length}] or N (default: 1): `)).trim();
|
|
599
|
+
if (ans.toUpperCase() === 'N') {
|
|
600
|
+
const chosen = await promptForFile(lines);
|
|
601
|
+
selectedFileObj = importFileToInput(chosen);
|
|
467
602
|
} else {
|
|
468
|
-
|
|
603
|
+
const num = parseInt(ans, 10);
|
|
604
|
+
if (!isNaN(num) && num >= 1 && num <= inputFiles.length) {
|
|
605
|
+
selectedFileObj = inputFiles[num - 1];
|
|
606
|
+
} else {
|
|
607
|
+
selectedFileObj = inputFiles[0];
|
|
608
|
+
}
|
|
469
609
|
}
|
|
470
|
-
} else {
|
|
471
|
-
selectedFileObj = inputFiles[0];
|
|
472
610
|
}
|
|
473
611
|
}
|
|
474
612
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrselva/attendance",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Extract vertical attendance data and export into DigiCampus Excel templates with ZIP archive",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node index.js"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [
|
|
9
|
+
"keywords": [
|
|
10
|
+
"attendance",
|
|
11
|
+
"digicampus",
|
|
12
|
+
"excel",
|
|
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",
|