@relipa/ai-flow-kit 0.1.0 → 0.1.1-beta.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 +497 -497
- package/bin/aiflow.js +525 -525
- package/custom/skills/figma-to-component/SKILL.md +294 -207
- package/custom/skills/read-study-requirement/SKILL.md +334 -334
- package/custom/templates/shared/gate-workflow.md +91 -91
- package/docs/common/CHANGELOG.md +334 -334
- package/docs/common/QUICK_START.md +537 -537
- package/docs/common/cli-reference.md +657 -657
- package/package.json +68 -67
- package/scripts/doctor.js +26 -0
- package/scripts/guide.js +314 -314
- package/scripts/hooks/session-start.js +315 -315
- package/scripts/prompt.js +95 -9
- package/scripts/score_members.js +320 -0
- package/scripts/use.js +1162 -1162
package/scripts/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
|
+
const { confirm } = require('@inquirer/prompts');
|
|
5
6
|
|
|
6
7
|
const PROJECT_DIR = process.cwd();
|
|
7
8
|
const CURRENT_FILE = path.join(PROJECT_DIR, '.aiflow', 'context', 'current.json');
|
|
@@ -288,15 +289,12 @@ module.exports = async function generatePrompt(type, options = {}) {
|
|
|
288
289
|
return;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
292
|
+
const clipResult = copyToClipboard(prompt);
|
|
293
|
+
if (clipResult.success) {
|
|
293
294
|
const ticket = ctx?.taskId ? chalk.gray(` [${ctx.taskId}]`) : '';
|
|
294
295
|
console.log(chalk.green(`✓ Prompt copied to clipboard!${ticket} Paste it into your AI chat to begin.`));
|
|
295
296
|
} else {
|
|
296
|
-
|
|
297
|
-
console.log(prompt);
|
|
298
|
-
console.log('─'.repeat(60) + '\n');
|
|
299
|
-
console.log(chalk.yellow('Could not copy to clipboard. Paste the prompt above manually.'));
|
|
297
|
+
await handleClipboardFailure(prompt, clipResult);
|
|
300
298
|
}
|
|
301
299
|
};
|
|
302
300
|
|
|
@@ -423,6 +421,33 @@ function isWSL() {
|
|
|
423
421
|
} catch (_) { return false; }
|
|
424
422
|
}
|
|
425
423
|
|
|
424
|
+
function detectLinuxPkgManager() {
|
|
425
|
+
const candidates = [
|
|
426
|
+
{ cmd: 'apt-get', installFlag: 'install -y' },
|
|
427
|
+
{ cmd: 'dnf', installFlag: 'install -y' },
|
|
428
|
+
{ cmd: 'pacman', installFlag: '-S --noconfirm' },
|
|
429
|
+
{ cmd: 'zypper', installFlag: 'install -y' },
|
|
430
|
+
];
|
|
431
|
+
for (const c of candidates) {
|
|
432
|
+
const r = spawnSync('which', [c.cmd], { stdio: 'ignore' });
|
|
433
|
+
if (r.status === 0) return c;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function linuxClipboardInfo() {
|
|
439
|
+
const isWayland = !!process.env.WAYLAND_DISPLAY;
|
|
440
|
+
const pkg = isWayland ? 'wl-clipboard' : 'xclip';
|
|
441
|
+
const mgr = detectLinuxPkgManager();
|
|
442
|
+
return {
|
|
443
|
+
pkg,
|
|
444
|
+
pkgManager: mgr ? mgr.cmd : null,
|
|
445
|
+
installArgs: mgr ? [...mgr.installFlag.split(' '), pkg] : null,
|
|
446
|
+
installDisplayCmd: mgr ? `sudo ${mgr.cmd} ${mgr.installFlag} ${pkg}` : null,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Returns { success: true } or { success: false, pkg, pkgManager, installArgs, installDisplayCmd }
|
|
426
451
|
function copyToClipboard(text) {
|
|
427
452
|
try {
|
|
428
453
|
if (process.platform === 'win32') {
|
|
@@ -432,8 +457,10 @@ function copyToClipboard(text) {
|
|
|
432
457
|
'[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())'],
|
|
433
458
|
{ input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] }
|
|
434
459
|
);
|
|
460
|
+
return { success: true };
|
|
435
461
|
} else if (process.platform === 'darwin') {
|
|
436
462
|
spawnSync('pbcopy', [], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
|
463
|
+
return { success: true };
|
|
437
464
|
} else if (isWSL()) {
|
|
438
465
|
spawnSync(
|
|
439
466
|
'powershell.exe',
|
|
@@ -441,12 +468,71 @@ function copyToClipboard(text) {
|
|
|
441
468
|
'[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())'],
|
|
442
469
|
{ input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] }
|
|
443
470
|
);
|
|
471
|
+
return { success: true };
|
|
444
472
|
} else {
|
|
473
|
+
// Try wl-copy (Wayland), then xclip, then xsel
|
|
474
|
+
if (process.env.WAYLAND_DISPLAY) {
|
|
475
|
+
const r = spawnSync('wl-copy', [], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
|
476
|
+
if (!r.error && r.status === 0) return { success: true };
|
|
477
|
+
}
|
|
445
478
|
const r = spawnSync('xclip', ['-selection', 'clipboard'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
|
446
|
-
if (r.error
|
|
479
|
+
if (!r.error && r.status === 0) return { success: true };
|
|
480
|
+
const r2 = spawnSync('xsel', ['--clipboard', '--input'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
|
481
|
+
if (!r2.error && r2.status === 0) return { success: true };
|
|
482
|
+
return { success: false, ...linuxClipboardInfo() };
|
|
483
|
+
}
|
|
484
|
+
} catch (_) {
|
|
485
|
+
return { success: false };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function showPromptFallback(text) {
|
|
490
|
+
console.log(chalk.cyan('\n' + '─'.repeat(60)));
|
|
491
|
+
console.log(text);
|
|
492
|
+
console.log(chalk.cyan('─'.repeat(60) + '\n'));
|
|
493
|
+
console.log(chalk.yellow('Clipboard unavailable — paste the prompt above into your AI chat.'));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function handleClipboardFailure(text, info) {
|
|
497
|
+
if (!info.pkg) {
|
|
498
|
+
// Unknown platform failure
|
|
499
|
+
showPromptFallback(text);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
console.log(chalk.yellow(`\n⚠ Clipboard tool not found (need: ${chalk.white(info.pkg)})`));
|
|
504
|
+
|
|
505
|
+
if (!info.installDisplayCmd) {
|
|
506
|
+
console.log(chalk.gray(' Could not detect a package manager. Install manually:'));
|
|
507
|
+
console.log(chalk.gray(' Wayland: sudo apt install wl-clipboard'));
|
|
508
|
+
console.log(chalk.gray(' X11: sudo apt install xclip\n'));
|
|
509
|
+
showPromptFallback(text);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
console.log(chalk.gray(` Install command: ${chalk.white(info.installDisplayCmd)}\n`));
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const doInstall = await confirm({
|
|
517
|
+
message: `Install ${info.pkg} now? (requires sudo)`,
|
|
518
|
+
default: true,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (doInstall) {
|
|
522
|
+
console.log(chalk.cyan(`\nRunning: sudo ${info.pkgManager} ${info.installArgs.join(' ')}`));
|
|
523
|
+
const r = spawnSync('sudo', [info.pkgManager, ...info.installArgs], { stdio: 'inherit' });
|
|
524
|
+
if (r.status === 0) {
|
|
525
|
+
const retry = copyToClipboard(text);
|
|
526
|
+
if (retry.success) {
|
|
527
|
+
console.log(chalk.green('\n✓ Installed! Prompt copied to clipboard. Paste it into your AI chat.'));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
console.log(chalk.red('\nInstallation failed. Showing prompt below:'));
|
|
447
532
|
}
|
|
448
|
-
return true;
|
|
449
533
|
} catch (_) {
|
|
450
|
-
|
|
534
|
+
// Non-interactive terminal (piped) — skip the question
|
|
451
535
|
}
|
|
536
|
+
|
|
537
|
+
showPromptFallback(text);
|
|
452
538
|
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const XLSX = require('xlsx');
|
|
7
|
+
|
|
8
|
+
const LOG_FILE = path.resolve(__dirname, '../tellog/log_25-05-2026_31-05-2026');
|
|
9
|
+
const OUTPUT_FILE = path.resolve(__dirname, '../tellog/log_25-05-2026_31-05-2026.xlsx');
|
|
10
|
+
|
|
11
|
+
const MEMBERS = [
|
|
12
|
+
'anhtt@relipasoft.com',
|
|
13
|
+
'hoangdv@relipasoft.com',
|
|
14
|
+
'chinhtt@relipasoft.com',
|
|
15
|
+
'phongnx@relipasoft.com',
|
|
16
|
+
'dungha@relipasoft.com',
|
|
17
|
+
'thangnv@relipasoft.com',
|
|
18
|
+
'nguyenlt@relipasoft.com',
|
|
19
|
+
'thuongvv@relipasoft.com',
|
|
20
|
+
'yenvtb@relipasoft.com',
|
|
21
|
+
'tructh@relipasoft.com',
|
|
22
|
+
'thainq@relipasoft.com',
|
|
23
|
+
'tuoittx@relipasoft.com',
|
|
24
|
+
'hungdv@relipasoft.com',
|
|
25
|
+
'hiepnk@relipasoft.com',
|
|
26
|
+
'khoihn@relipasoft.com',
|
|
27
|
+
'huynq@relipasoft.com',
|
|
28
|
+
'thinhvq@relipasoft.com',
|
|
29
|
+
'banv@relipasoft.com',
|
|
30
|
+
'duongnt1@relipasoft.com',
|
|
31
|
+
'anhnt@relipasoft.com',
|
|
32
|
+
'khanhdd@relipasoft.com',
|
|
33
|
+
'hieunv@relipasoft.com',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const EXPLORE_COMMANDS = new Set(['init', 'update', 'version', 'help', 'init.help', 'unknown_flag']);
|
|
37
|
+
const USE_COMMANDS = new Set(['use', 'prompt', 'guide', 'guideline', 'guiline', 'guide?']);
|
|
38
|
+
|
|
39
|
+
// Tickets that are clearly not real work (test, demo, throwaway)
|
|
40
|
+
const FAKE_TICKET_PATTERNS = /^(test|test-direct|demo|investigation|sample|trial|dummy)(-.*)?$/i;
|
|
41
|
+
|
|
42
|
+
function isRealTicket(ticket) {
|
|
43
|
+
if (!ticket || ticket === '-' || ticket.length < 3) return false;
|
|
44
|
+
if (FAKE_TICKET_PATTERNS.test(ticket)) return false;
|
|
45
|
+
// Real tickets typically follow PROJECT-ID pattern with uppercase prefix
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseLog() {
|
|
50
|
+
const content = fs.readFileSync(LOG_FILE, 'utf8');
|
|
51
|
+
const lines = content.split('\n');
|
|
52
|
+
|
|
53
|
+
// Skip header (line 0)
|
|
54
|
+
const events = [];
|
|
55
|
+
for (let i = 1; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i].trim();
|
|
57
|
+
if (!line) continue;
|
|
58
|
+
|
|
59
|
+
// TSV format: columns separated by tabs
|
|
60
|
+
const cols = line.split('\t');
|
|
61
|
+
if (cols.length < 7) continue;
|
|
62
|
+
|
|
63
|
+
const timestamp = cols[0];
|
|
64
|
+
const repo = cols[1];
|
|
65
|
+
const user = cols[2];
|
|
66
|
+
const command = (cols[6] || '').trim();
|
|
67
|
+
const gate = (cols[8] || '').trim();
|
|
68
|
+
const ticket = (cols[9] || '').trim();
|
|
69
|
+
|
|
70
|
+
if (!timestamp || !user) continue;
|
|
71
|
+
|
|
72
|
+
// Parse date from "DD/MM/YYYY HH:MM" format
|
|
73
|
+
const dateMatch = timestamp.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
|
|
74
|
+
if (!dateMatch) continue;
|
|
75
|
+
const dateStr = `${dateMatch[3]}-${dateMatch[2]}-${dateMatch[1]}`;
|
|
76
|
+
|
|
77
|
+
events.push({ timestamp, dateStr, repo, user, command, gate, ticket });
|
|
78
|
+
}
|
|
79
|
+
return events;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detectSessions(userEvents) {
|
|
83
|
+
// A new session = gap of more than 30 minutes between consecutive events
|
|
84
|
+
// or a new day; simplified: count distinct hour blocks per day
|
|
85
|
+
const sessionKeys = new Set();
|
|
86
|
+
for (const e of userEvents) {
|
|
87
|
+
const hourMatch = e.timestamp.match(/(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):/);
|
|
88
|
+
if (hourMatch) {
|
|
89
|
+
sessionKeys.add(`${hourMatch[3]}-${hourMatch[2]}-${hourMatch[1]}-${hourMatch[4]}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return sessionKeys.size;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function score(stats) {
|
|
96
|
+
const { totalEvents, activeDays, sessions, gateStarts, gateApproveds, ticketsProcessed,
|
|
97
|
+
ticketsInWeek, hasUseOrPrompt, hasGateStartWithTicket, hasGate3to5Approved } = stats;
|
|
98
|
+
|
|
99
|
+
if (totalEvents === 0) return { depth: 0, depthLabel: 'Không hoạt động' };
|
|
100
|
+
|
|
101
|
+
const hasOnlyExplore = stats.onlyExploreCommands;
|
|
102
|
+
|
|
103
|
+
// Score 5: Gate 3-5 approved + >= 3 real tickets active in week
|
|
104
|
+
if (hasGate3to5Approved && ticketsInWeek >= 3) {
|
|
105
|
+
return { depth: 5, depthLabel: 'Sử dụng thường xuyên workflow' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Score 4: gate.approved >= 1 + >= 1 ticket processed to end
|
|
109
|
+
if (gateApproveds >= 1 && ticketsProcessed >= 1) {
|
|
110
|
+
return { depth: 4, depthLabel: 'Hoàn thành workflow' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Score 3: gate.start with real ticket + >= 3 sessions
|
|
114
|
+
if (hasGateStartWithTicket && sessions >= 3) {
|
|
115
|
+
return { depth: 3, depthLabel: 'Dùng có workflow' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Score 2: has use/prompt/guideline OR >= 2 sessions
|
|
119
|
+
if (hasUseOrPrompt || sessions >= 2) {
|
|
120
|
+
return { depth: 2, depthLabel: 'Bắt đầu dùng' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Score 1: only explore commands
|
|
124
|
+
if (hasOnlyExplore) {
|
|
125
|
+
return { depth: 1, depthLabel: 'Mới khám phá' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { depth: 1, depthLabel: 'Mới khám phá' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function computeImpactScore(stats) {
|
|
132
|
+
// Impact: based on tickets processed and gate depth
|
|
133
|
+
const { ticketsProcessed, gateApproveds, hasGate3to5Approved } = stats;
|
|
134
|
+
if (ticketsProcessed >= 3 && hasGate3to5Approved) return { impact: 5, impactLabel: 'Rất cao' };
|
|
135
|
+
if (ticketsProcessed >= 2 && gateApproveds >= 2) return { impact: 4, impactLabel: 'Cao' };
|
|
136
|
+
if (ticketsProcessed >= 1 && gateApproveds >= 1) return { impact: 3, impactLabel: 'Trung bình' };
|
|
137
|
+
if (stats.gateStarts >= 1) return { impact: 2, impactLabel: 'Thấp' };
|
|
138
|
+
if (stats.totalEvents > 0) return { impact: 1, impactLabel: 'Rất thấp' };
|
|
139
|
+
return { impact: 0, impactLabel: 'Không có' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildNotes(stats) {
|
|
143
|
+
const parts = [];
|
|
144
|
+
if (stats.hasGate3to5Approved) parts.push('Gate 3-5 approved');
|
|
145
|
+
if (stats.ticketsProcessed > 0) parts.push(`${stats.ticketsProcessed} ticket(s) hoàn thành`);
|
|
146
|
+
if (stats.ticketsInWeek > stats.ticketsProcessed) parts.push(`${stats.ticketsInWeek} ticket(s) trong tuần`);
|
|
147
|
+
if (stats.gateStarts > 0 && stats.ticketsProcessed === 0) parts.push('Có gate.start nhưng chưa hoàn thành ticket');
|
|
148
|
+
if (stats.onlyExploreCommands && stats.totalEvents > 0) parts.push('Chỉ dùng lệnh khám phá');
|
|
149
|
+
if (stats.totalEvents === 0) parts.push('Không có hoạt động');
|
|
150
|
+
return parts.join('; ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function analyzeMembers(events) {
|
|
154
|
+
const results = [];
|
|
155
|
+
|
|
156
|
+
for (const email of MEMBERS) {
|
|
157
|
+
const userEvents = events.filter(e => e.user === email);
|
|
158
|
+
const totalEvents = userEvents.length;
|
|
159
|
+
|
|
160
|
+
const activeDaysSet = new Set(userEvents.map(e => e.dateStr));
|
|
161
|
+
const activeDays = activeDaysSet.size;
|
|
162
|
+
|
|
163
|
+
const sessions = detectSessions(userEvents);
|
|
164
|
+
|
|
165
|
+
// Collect unique commands (non-empty, non-dash)
|
|
166
|
+
const commandSet = new Set(
|
|
167
|
+
userEvents.map(e => e.command).filter(c => c && c !== '-')
|
|
168
|
+
);
|
|
169
|
+
const keyCommands = [...commandSet].join(', ');
|
|
170
|
+
|
|
171
|
+
// Gate starts (gate1.start, gate2.start, ...)
|
|
172
|
+
const gateStartEvents = userEvents.filter(e => e.command && e.command.match(/^gate\d+\.start$/));
|
|
173
|
+
const gateStarts = gateStartEvents.length;
|
|
174
|
+
|
|
175
|
+
// Gate approved
|
|
176
|
+
const gateApprovedEvents = userEvents.filter(e => e.command && e.command.match(/^gate\d+\.approved$/));
|
|
177
|
+
const gateApproveds = gateApprovedEvents.length;
|
|
178
|
+
|
|
179
|
+
// Tickets processed to end (reached gate4.approved or gate5.start) - real tickets only
|
|
180
|
+
const completedTicketEvents = userEvents.filter(e =>
|
|
181
|
+
e.command && (e.command === 'gate4.approved' || e.command === 'gate5.start') && isRealTicket(e.ticket)
|
|
182
|
+
);
|
|
183
|
+
const ticketsProcessedSet = new Set(completedTicketEvents.map(e => e.ticket));
|
|
184
|
+
const ticketsProcessed = ticketsProcessedSet.size;
|
|
185
|
+
|
|
186
|
+
// Tickets active in week (any gate event with real ticket)
|
|
187
|
+
const ticketsInWeekSet = new Set(
|
|
188
|
+
userEvents
|
|
189
|
+
.filter(e => e.command && e.command.match(/^gate\d+\.(start|approved)$/) && isRealTicket(e.ticket))
|
|
190
|
+
.map(e => e.ticket)
|
|
191
|
+
);
|
|
192
|
+
const ticketsInWeek = ticketsInWeekSet.size;
|
|
193
|
+
|
|
194
|
+
const hasUseOrPrompt = userEvents.some(e =>
|
|
195
|
+
e.command && (USE_COMMANDS.has(e.command) || e.command.startsWith('guide'))
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const hasGateStartWithRealTicket = gateStartEvents.some(e => isRealTicket(e.ticket));
|
|
199
|
+
|
|
200
|
+
// Gate 3-5 approved
|
|
201
|
+
const hasGate3to5Approved = userEvents.some(e =>
|
|
202
|
+
e.command && e.command.match(/^gate[345]\.approved$/)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const nonExploreCommands = userEvents.filter(e =>
|
|
206
|
+
e.command && e.command !== '-' && !EXPLORE_COMMANDS.has(e.command)
|
|
207
|
+
);
|
|
208
|
+
const onlyExploreCommands = totalEvents > 0 && nonExploreCommands.length === 0;
|
|
209
|
+
|
|
210
|
+
const stats = {
|
|
211
|
+
totalEvents, activeDays, sessions, gateStarts, gateApproveds,
|
|
212
|
+
ticketsProcessed, ticketsInWeek, hasUseOrPrompt, hasGateStartWithTicket: hasGateStartWithRealTicket,
|
|
213
|
+
hasGate3to5Approved, onlyExploreCommands
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const { depth, depthLabel } = score(stats);
|
|
217
|
+
const { impact, impactLabel } = computeImpactScore(stats);
|
|
218
|
+
const totalScore = depth + impact;
|
|
219
|
+
const notes = buildNotes(stats);
|
|
220
|
+
|
|
221
|
+
results.push({
|
|
222
|
+
email, totalEvents, activeDays, keyCommands,
|
|
223
|
+
gateStarts, gateApproveds, ticketsProcessed,
|
|
224
|
+
depth, depthLabel, impact, impactLabel, totalScore, notes
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildExcel(results) {
|
|
232
|
+
const wb = XLSX.utils.book_new();
|
|
233
|
+
|
|
234
|
+
const headers = [
|
|
235
|
+
'#', 'Email', 'Events', 'Active\nDays', 'Key Commands',
|
|
236
|
+
'Gate Start', 'Gate Approved', 'Tickets Processed',
|
|
237
|
+
'Depth\nScore', 'Depth Label',
|
|
238
|
+
'Impact\nScore', 'Impact Label',
|
|
239
|
+
'Total\nScore', 'Notes'
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const rows = [headers];
|
|
243
|
+
results.forEach((r, i) => {
|
|
244
|
+
rows.push([
|
|
245
|
+
i + 1,
|
|
246
|
+
r.email,
|
|
247
|
+
r.totalEvents,
|
|
248
|
+
r.activeDays,
|
|
249
|
+
r.keyCommands,
|
|
250
|
+
r.gateStarts,
|
|
251
|
+
r.gateApproveds,
|
|
252
|
+
r.ticketsProcessed,
|
|
253
|
+
r.depth,
|
|
254
|
+
r.depthLabel,
|
|
255
|
+
r.impact,
|
|
256
|
+
r.impactLabel,
|
|
257
|
+
r.totalScore,
|
|
258
|
+
r.notes
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const ws = XLSX.utils.aoa_to_sheet(rows);
|
|
263
|
+
|
|
264
|
+
// Column widths
|
|
265
|
+
ws['!cols'] = [
|
|
266
|
+
{ wch: 4 }, // #
|
|
267
|
+
{ wch: 32 }, // Email
|
|
268
|
+
{ wch: 8 }, // Events
|
|
269
|
+
{ wch: 8 }, // Active Days
|
|
270
|
+
{ wch: 50 }, // Key Commands
|
|
271
|
+
{ wch: 10 }, // Gate Start
|
|
272
|
+
{ wch: 13 }, // Gate Approved
|
|
273
|
+
{ wch: 16 }, // Tickets Processed
|
|
274
|
+
{ wch: 8 }, // Depth Score
|
|
275
|
+
{ wch: 28 }, // Depth Label
|
|
276
|
+
{ wch: 8 }, // Impact Score
|
|
277
|
+
{ wch: 14 }, // Impact Label
|
|
278
|
+
{ wch: 8 }, // Total Score
|
|
279
|
+
{ wch: 50 }, // Notes
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
XLSX.utils.book_append_sheet(wb, ws, 'Scoring');
|
|
283
|
+
XLSX.writeFile(wb, OUTPUT_FILE);
|
|
284
|
+
console.log(`✅ Excel saved to: ${OUTPUT_FILE}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const events = parseLog();
|
|
288
|
+
console.log(`Parsed ${events.length} events`);
|
|
289
|
+
|
|
290
|
+
const results = analyzeMembers(events);
|
|
291
|
+
buildExcel(results);
|
|
292
|
+
|
|
293
|
+
// Print summary
|
|
294
|
+
console.log('\n=== SCORING SUMMARY ===');
|
|
295
|
+
console.log(
|
|
296
|
+
'#'.padEnd(3),
|
|
297
|
+
'Email'.padEnd(34),
|
|
298
|
+
'Events'.padEnd(7),
|
|
299
|
+
'Days'.padEnd(5),
|
|
300
|
+
'GS'.padEnd(4),
|
|
301
|
+
'GA'.padEnd(4),
|
|
302
|
+
'TP'.padEnd(4),
|
|
303
|
+
'Depth'.padEnd(6),
|
|
304
|
+
'Impact'.padEnd(7),
|
|
305
|
+
'Total'
|
|
306
|
+
);
|
|
307
|
+
results.forEach((r, i) => {
|
|
308
|
+
console.log(
|
|
309
|
+
String(i + 1).padEnd(3),
|
|
310
|
+
r.email.padEnd(34),
|
|
311
|
+
String(r.totalEvents).padEnd(7),
|
|
312
|
+
String(r.activeDays).padEnd(5),
|
|
313
|
+
String(r.gateStarts).padEnd(4),
|
|
314
|
+
String(r.gateApproveds).padEnd(4),
|
|
315
|
+
String(r.ticketsProcessed).padEnd(4),
|
|
316
|
+
`${r.depth}(${r.depthLabel.slice(0, 4)})`.padEnd(6),
|
|
317
|
+
String(r.impact).padEnd(7),
|
|
318
|
+
r.totalScore
|
|
319
|
+
);
|
|
320
|
+
});
|