@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.js +154 -16
  3. package/package.json +9 -3
package/README.md CHANGED
@@ -29,7 +29,7 @@ attendance
29
29
 
30
30
  ## ✨ Features
31
31
 
32
- - **Zero Configuration**: Automatically discovers `.xlsx` or `.xls` spreadsheets in `./input` or the current working directory.
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
- selectedFileObj = { name: path.basename(directPath), fullPath: directPath, relativePath: flags.file };
565
+ resolvedSrc = directPath;
442
566
  } else if (fs.existsSync(inInputPath) && fs.statSync(inInputPath).isFile()) {
443
- selectedFileObj = { name: path.basename(inInputPath), fullPath: inInputPath, relativePath: path.join('input', flags.file) };
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 > 1) {
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
- const ans = await askQuestion(lines, `\nSelect file [1-${inputFiles.length}] (default: 1): `);
464
- const num = parseInt(ans.trim(), 10);
465
- if (!isNaN(num) && num >= 1 && num <= inputFiles.length) {
466
- selectedFileObj = inputFiles[num - 1];
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
- selectedFileObj = inputFiles[0];
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.0.0",
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": ["attendance", "digicampus", "excel", "cli", "npx"],
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": "./index.js"
22
+ "attendance": "index.js"
17
23
  },
18
24
  "files": [
19
25
  "index.js",