auto-clock-cli 1.0.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/clock.js +1050 -0
- package/config.example.json +5 -0
- package/package.json +36 -0
- package/readme.md +96 -0
package/clock.js
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
// Store config in user's home directory
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.auto-clock');
|
|
11
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
12
|
+
const LOG_PATH = path.join(CONFIG_DIR, 'clock.log');
|
|
13
|
+
const API_BASE = 'https://api-hrms.techbodia.com/api';
|
|
14
|
+
|
|
15
|
+
// Ensure config directory exists
|
|
16
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
17
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helper to prompt user input
|
|
21
|
+
function prompt(question) {
|
|
22
|
+
const rl = readline.createInterface({
|
|
23
|
+
input: process.stdin,
|
|
24
|
+
output: process.stdout,
|
|
25
|
+
});
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
rl.question(question, (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(answer.trim());
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Load config
|
|
35
|
+
function loadConfig() {
|
|
36
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Save config
|
|
43
|
+
function saveConfig(config) {
|
|
44
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Log to file
|
|
48
|
+
function log(message) {
|
|
49
|
+
const timestamp = new Date().toISOString();
|
|
50
|
+
const logMessage = `[${timestamp}] ${message}\n`;
|
|
51
|
+
fs.appendFileSync(LOG_PATH, logMessage);
|
|
52
|
+
console.log(message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Login and get fresh JWT
|
|
56
|
+
async function login(email, password, silent = false) {
|
|
57
|
+
if (!silent) console.log(`Logging in as ${email}...`);
|
|
58
|
+
|
|
59
|
+
const response = await fetch(`${API_BASE}/Auth/Login`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
Email: email,
|
|
64
|
+
Password: password,
|
|
65
|
+
visitorId: 'auto-clock-cli',
|
|
66
|
+
requestId: Date.now().toString(),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
|
|
72
|
+
if (data.ErrorCode !== 0) {
|
|
73
|
+
throw new Error(data.ErrorMessage || 'Login failed');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!silent) console.log(`Welcome, ${data.Result.Username}!`);
|
|
77
|
+
return { jwt: data.Result.Jwt, username: data.Result.Username };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Do clock in/out (using curl because fetch has issues with this API)
|
|
81
|
+
function doClock(jwt, isClockIn, location = 'TechBodia') {
|
|
82
|
+
const body = JSON.stringify({
|
|
83
|
+
IsClockIn: isClockIn,
|
|
84
|
+
IsInCompany: true,
|
|
85
|
+
Location: location,
|
|
86
|
+
ClockInRemark: '',
|
|
87
|
+
ClockOutRemark: '',
|
|
88
|
+
TimeZone: '07:00',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const cmd = `curl -s -X POST '${API_BASE}/Member/DoClock' \
|
|
92
|
+
-H 'accept: text/plain' \
|
|
93
|
+
-H 'Authorization: Bearer Bearer ${jwt}' \
|
|
94
|
+
-H 'Content-Type: application/json-patch+json' \
|
|
95
|
+
-d '${body}'`;
|
|
96
|
+
|
|
97
|
+
const result = execSync(cmd, { encoding: 'utf-8' });
|
|
98
|
+
const data = JSON.parse(result);
|
|
99
|
+
|
|
100
|
+
// Return the result with status info
|
|
101
|
+
return {
|
|
102
|
+
success: data.ErrorCode === 0,
|
|
103
|
+
alreadyDone: data.ErrorCode === 14 || data.ErrorCode === 15,
|
|
104
|
+
message: data.ErrorMessage,
|
|
105
|
+
errorCode: data.ErrorCode,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Interactive setup wizard
|
|
110
|
+
async function setup() {
|
|
111
|
+
console.log('\n========================================');
|
|
112
|
+
console.log(' Auto Clock - Setup Wizard');
|
|
113
|
+
console.log('========================================\n');
|
|
114
|
+
|
|
115
|
+
const email = await prompt('Enter your email: ');
|
|
116
|
+
const password = await prompt('Enter your password: ');
|
|
117
|
+
const location = (await prompt('Enter location (default: TechBodia): ')) || 'TechBodia';
|
|
118
|
+
const clockInTime = (await prompt('Clock in time (HH:MM, default: 08:00): ')) || '08:00';
|
|
119
|
+
const clockOutTime = (await prompt('Clock out time (HH:MM, default: 17:00): ')) || '17:00';
|
|
120
|
+
|
|
121
|
+
console.log('\nVerifying credentials...');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const { jwt, username } = await login(email, password);
|
|
125
|
+
|
|
126
|
+
const config = {
|
|
127
|
+
email,
|
|
128
|
+
password,
|
|
129
|
+
location,
|
|
130
|
+
clockInTime,
|
|
131
|
+
clockOutTime,
|
|
132
|
+
jwt,
|
|
133
|
+
username,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
saveConfig(config);
|
|
137
|
+
|
|
138
|
+
console.log('\n----------------------------------------');
|
|
139
|
+
console.log('Setup complete!');
|
|
140
|
+
console.log('----------------------------------------');
|
|
141
|
+
console.log(`User: ${username}`);
|
|
142
|
+
console.log(`Location: ${location}`);
|
|
143
|
+
console.log(`Clock In: ${clockInTime}`);
|
|
144
|
+
console.log(`Clock Out: ${clockOutTime}`);
|
|
145
|
+
console.log(`Config saved to: ${CONFIG_PATH}`);
|
|
146
|
+
console.log('\nRun "auto-clock cron" to see scheduling instructions.');
|
|
147
|
+
console.log('----------------------------------------\n');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('\nSetup failed:', error.message);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Show cron instructions
|
|
155
|
+
function showCron() {
|
|
156
|
+
const config = loadConfig();
|
|
157
|
+
if (!config) {
|
|
158
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const nodePath = process.execPath;
|
|
163
|
+
const scriptPath = path.join(__dirname, 'clock.js');
|
|
164
|
+
|
|
165
|
+
const [inHour, inMin] = config.clockInTime.split(':');
|
|
166
|
+
const [outHour, outMin] = config.clockOutTime.split(':');
|
|
167
|
+
|
|
168
|
+
console.log('\n========================================');
|
|
169
|
+
console.log(' Cron Setup Instructions');
|
|
170
|
+
console.log('========================================\n');
|
|
171
|
+
|
|
172
|
+
console.log('1. Open crontab editor:');
|
|
173
|
+
console.log(' crontab -e\n');
|
|
174
|
+
|
|
175
|
+
console.log('2. Add these lines:\n');
|
|
176
|
+
console.log('# Auto Clock - Clock In');
|
|
177
|
+
console.log(`${inMin} ${inHour} * * 1-5 ${nodePath} ${scriptPath} in >> ${LOG_PATH} 2>&1`);
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log('# Auto Clock - Clock Out');
|
|
180
|
+
console.log(`${outMin} ${outHour} * * 1-5 ${nodePath} ${scriptPath} out >> ${LOG_PATH} 2>&1`);
|
|
181
|
+
|
|
182
|
+
console.log('\n3. Save and exit (:wq in vim)\n');
|
|
183
|
+
|
|
184
|
+
console.log('Your schedule:');
|
|
185
|
+
console.log(` Clock In: ${config.clockInTime} (Mon-Fri)`);
|
|
186
|
+
console.log(` Clock Out: ${config.clockOutTime} (Mon-Fri)\n`);
|
|
187
|
+
|
|
188
|
+
console.log('To verify: crontab -l');
|
|
189
|
+
console.log('To view logs: cat ' + LOG_PATH);
|
|
190
|
+
console.log('----------------------------------------\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Set schedule times
|
|
194
|
+
async function setSchedule() {
|
|
195
|
+
const config = loadConfig();
|
|
196
|
+
if (!config) {
|
|
197
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`\nCurrent schedule: In=${config.clockInTime}, Out=${config.clockOutTime}\n`);
|
|
202
|
+
|
|
203
|
+
const clockInTime = (await prompt(`New clock in time (HH:MM, current: ${config.clockInTime}): `)) || config.clockInTime;
|
|
204
|
+
const clockOutTime = (await prompt(`New clock out time (HH:MM, current: ${config.clockOutTime}): `)) || config.clockOutTime;
|
|
205
|
+
|
|
206
|
+
config.clockInTime = clockInTime;
|
|
207
|
+
config.clockOutTime = clockOutTime;
|
|
208
|
+
saveConfig(config);
|
|
209
|
+
|
|
210
|
+
console.log(`\nSchedule updated: In=${clockInTime}, Out=${clockOutTime}`);
|
|
211
|
+
console.log('Run "auto-clock cron" to update your cron jobs.\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Show current status
|
|
215
|
+
function showStatus() {
|
|
216
|
+
const config = loadConfig();
|
|
217
|
+
if (!config) {
|
|
218
|
+
console.log('\nStatus: Not configured');
|
|
219
|
+
console.log('Run "auto-clock setup" to get started.\n');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check if cron is enabled
|
|
224
|
+
let cronEnabled = false;
|
|
225
|
+
let cronType = '';
|
|
226
|
+
try {
|
|
227
|
+
const currentCron = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
228
|
+
if (currentCron.includes('clock.js smart')) {
|
|
229
|
+
cronEnabled = true;
|
|
230
|
+
cronType = 'Smart Mode';
|
|
231
|
+
} else if (currentCron.includes('clock.js in') || currentCron.includes('clock.js out')) {
|
|
232
|
+
cronEnabled = true;
|
|
233
|
+
cronType = 'Fixed Time';
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
// No crontab
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('\n========================================');
|
|
240
|
+
console.log(' Auto Clock Status');
|
|
241
|
+
console.log('========================================');
|
|
242
|
+
console.log(`User: ${config.username || 'Unknown'}`);
|
|
243
|
+
console.log(`Email: ${config.email}`);
|
|
244
|
+
console.log(`Location: ${config.location}`);
|
|
245
|
+
console.log(`Clock In: ${config.clockInTime}`);
|
|
246
|
+
console.log(`Clock Out: ${config.clockOutTime}`);
|
|
247
|
+
console.log('----------------------------------------');
|
|
248
|
+
if (cronEnabled) {
|
|
249
|
+
console.log(`Auto Clock: ENABLED (${cronType})`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log(`Auto Clock: DISABLED`);
|
|
252
|
+
}
|
|
253
|
+
console.log('----------------------------------------');
|
|
254
|
+
console.log(`Config: ${CONFIG_PATH}`);
|
|
255
|
+
console.log(`Log: ${LOG_PATH}`);
|
|
256
|
+
console.log('========================================\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Perform clock in/out
|
|
260
|
+
async function performClock(isClockIn) {
|
|
261
|
+
const config = loadConfig();
|
|
262
|
+
if (!config) {
|
|
263
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const action = isClockIn ? 'Clock In' : 'Clock Out';
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
// Try to login to get fresh JWT
|
|
271
|
+
let jwt = config.jwt;
|
|
272
|
+
try {
|
|
273
|
+
const result = await login(config.email, config.password, true);
|
|
274
|
+
if (result.jwt) {
|
|
275
|
+
jwt = result.jwt;
|
|
276
|
+
config.jwt = jwt;
|
|
277
|
+
saveConfig(config);
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// Login failed, use existing token
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!jwt) {
|
|
284
|
+
throw new Error('No JWT token. Run "auto-clock token" to set one.');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Do clock
|
|
288
|
+
const result = doClock(jwt, isClockIn, config.location);
|
|
289
|
+
const user = config.username || config.email;
|
|
290
|
+
|
|
291
|
+
if (result.success) {
|
|
292
|
+
log(`${action} successful for ${user}`);
|
|
293
|
+
return true;
|
|
294
|
+
} else if (result.alreadyDone) {
|
|
295
|
+
log(`${action}: ${result.message} (${user})`);
|
|
296
|
+
return true; // Still considered success
|
|
297
|
+
} else {
|
|
298
|
+
throw new Error(result.message || 'Unknown error');
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
log(`${action} FAILED: ${error.message}`);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Set JWT token manually
|
|
307
|
+
async function setToken() {
|
|
308
|
+
const config = loadConfig() || {};
|
|
309
|
+
|
|
310
|
+
console.log('\n========================================');
|
|
311
|
+
console.log(' Set JWT Token Manually');
|
|
312
|
+
console.log('========================================\n');
|
|
313
|
+
console.log('Get your JWT token from the HRMS web app:');
|
|
314
|
+
console.log('1. Login to HRMS website');
|
|
315
|
+
console.log('2. Open DevTools (F12) → Network tab');
|
|
316
|
+
console.log('3. Click any API request → Headers → Authorization');
|
|
317
|
+
console.log('4. Copy the token (after "Bearer ")\n');
|
|
318
|
+
|
|
319
|
+
const jwt = await prompt('Paste your JWT token: ');
|
|
320
|
+
|
|
321
|
+
if (!jwt) {
|
|
322
|
+
console.error('No token provided.');
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
config.jwt = jwt;
|
|
327
|
+
|
|
328
|
+
// If no email set, ask for basic info
|
|
329
|
+
if (!config.email) {
|
|
330
|
+
config.email = await prompt('Enter your email: ');
|
|
331
|
+
config.location = (await prompt('Enter location (default: TechBodia): ')) || 'TechBodia';
|
|
332
|
+
config.clockInTime = (await prompt('Clock in time (HH:MM, default: 08:00): ')) || '08:00';
|
|
333
|
+
config.clockOutTime = (await prompt('Clock out time (HH:MM, default: 17:00): ')) || '17:00';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
saveConfig(config);
|
|
337
|
+
console.log('\nToken saved! Try "auto-clock in" to test.\n');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Show help
|
|
341
|
+
function showHelp() {
|
|
342
|
+
console.log(`
|
|
343
|
+
Auto Clock CLI v1.0.0
|
|
344
|
+
=====================
|
|
345
|
+
|
|
346
|
+
Usage: auto-clock <command>
|
|
347
|
+
|
|
348
|
+
Commands:
|
|
349
|
+
setup Interactive setup wizard (first-time users)
|
|
350
|
+
token Manually set JWT token (if login doesn't return JWT)
|
|
351
|
+
in Clock in now
|
|
352
|
+
out Clock out now
|
|
353
|
+
smart Smart auto clock (checks if clock in/out is needed)
|
|
354
|
+
history [days] Show clock in/out history (default: 14 days)
|
|
355
|
+
reclock Request reclock for a single day
|
|
356
|
+
reclock-range Request reclock for multiple days (date range)
|
|
357
|
+
enable Enable auto clock (add cron jobs)
|
|
358
|
+
disable Disable auto clock (remove cron jobs)
|
|
359
|
+
schedule Change clock in/out times
|
|
360
|
+
cron Show cron setup instructions (manual)
|
|
361
|
+
status Show current configuration
|
|
362
|
+
logs Show recent logs
|
|
363
|
+
help Show this help message
|
|
364
|
+
|
|
365
|
+
Examples:
|
|
366
|
+
auto-clock setup # First-time setup
|
|
367
|
+
auto-clock token # Manually set JWT token
|
|
368
|
+
auto-clock in # Manual clock in
|
|
369
|
+
auto-clock out # Manual clock out
|
|
370
|
+
auto-clock smart # Smart clock (auto detects if in/out needed)
|
|
371
|
+
auto-clock history # Check clock history (14 days)
|
|
372
|
+
auto-clock history 7 # Check clock history (7 days)
|
|
373
|
+
auto-clock reclock # Request reclock for a single day
|
|
374
|
+
auto-clock reclock-range # Request reclock for multiple days
|
|
375
|
+
auto-clock enable # Enable smart auto clock (runs every 10 min)
|
|
376
|
+
auto-clock disable # Disable auto clock
|
|
377
|
+
auto-clock schedule # Change your times
|
|
378
|
+
auto-clock cron # Show cron commands (manual setup)
|
|
379
|
+
|
|
380
|
+
Config location: ${CONFIG_PATH}
|
|
381
|
+
`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Show logs
|
|
385
|
+
function showLogs() {
|
|
386
|
+
if (!fs.existsSync(LOG_PATH)) {
|
|
387
|
+
console.log('\nNo logs yet.\n');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log('\n========================================');
|
|
392
|
+
console.log(' Recent Logs');
|
|
393
|
+
console.log('========================================\n');
|
|
394
|
+
|
|
395
|
+
const logs = fs.readFileSync(LOG_PATH, 'utf-8');
|
|
396
|
+
const lines = logs.trim().split('\n').slice(-20);
|
|
397
|
+
console.log(lines.join('\n'));
|
|
398
|
+
console.log('\n========================================\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Format date with timezone offset +07:00
|
|
402
|
+
function formatDateWithTZ(date) {
|
|
403
|
+
const year = date.getFullYear();
|
|
404
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
405
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
406
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
407
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
408
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
409
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+07:00`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Get time table history
|
|
413
|
+
function getHistory(jwt, days = 7) {
|
|
414
|
+
const endDate = new Date();
|
|
415
|
+
const startDate = new Date();
|
|
416
|
+
startDate.setDate(startDate.getDate() - days);
|
|
417
|
+
|
|
418
|
+
const body = JSON.stringify({
|
|
419
|
+
StartDate: formatDateWithTZ(startDate),
|
|
420
|
+
EndDate: formatDateWithTZ(endDate),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const cmd = `curl -s -X POST '${API_BASE}/Member/GetTimeTable' \
|
|
424
|
+
-H 'accept: text/plain' \
|
|
425
|
+
-H 'Authorization: Bearer Bearer ${jwt}' \
|
|
426
|
+
-H 'Content-Type: application/json-patch+json' \
|
|
427
|
+
-d '${body}'`;
|
|
428
|
+
|
|
429
|
+
const result = execSync(cmd, { encoding: 'utf-8' });
|
|
430
|
+
return JSON.parse(result);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Show history command
|
|
434
|
+
async function showHistory(days = 14) {
|
|
435
|
+
const config = loadConfig();
|
|
436
|
+
if (!config) {
|
|
437
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Try to get fresh JWT
|
|
442
|
+
let jwt = config.jwt;
|
|
443
|
+
try {
|
|
444
|
+
const result = await login(config.email, config.password, true);
|
|
445
|
+
if (result.jwt) {
|
|
446
|
+
jwt = result.jwt;
|
|
447
|
+
config.jwt = jwt;
|
|
448
|
+
saveConfig(config);
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
// Use existing token
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!jwt) {
|
|
455
|
+
console.error('No JWT token. Run "auto-clock token" to set one.');
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const data = getHistory(jwt, days);
|
|
460
|
+
|
|
461
|
+
if (data.ErrorCode !== 0) {
|
|
462
|
+
console.error('Failed to get history:', data.ErrorMessage);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log('\n╔══════════════════════════════════════════════════════════════════════╗');
|
|
467
|
+
console.log(`║ Clock History (Last ${String(days).padStart(2)} days) ║`);
|
|
468
|
+
console.log('╠════════════╦══════════╦═══════════╦══════════╦═══════════════════════╣');
|
|
469
|
+
console.log('║ Date ║ Clock In ║ Clock Out ║ Status ║ Notes ║');
|
|
470
|
+
console.log('╠════════════╬══════════╬═══════════╬══════════╬═══════════════════════╣');
|
|
471
|
+
|
|
472
|
+
for (const day of data.Result) {
|
|
473
|
+
const date = day.WorkDate;
|
|
474
|
+
const clockIn = (day.ClockIn || '--:--').padStart(5).padEnd(5);
|
|
475
|
+
const clockOut = (day.ClockOut || '--:--').padStart(5).padEnd(5);
|
|
476
|
+
|
|
477
|
+
let status = '';
|
|
478
|
+
let notes = '';
|
|
479
|
+
|
|
480
|
+
// Determine status
|
|
481
|
+
if (day.LeaveInfo) {
|
|
482
|
+
status = 'Leave';
|
|
483
|
+
notes = 'On Leave';
|
|
484
|
+
} else if (!day.ClockIn && !day.ClockOut) {
|
|
485
|
+
status = 'Missing';
|
|
486
|
+
if (day.ReClockIn) {
|
|
487
|
+
notes = `ReClock: ${day.ReClockIn.Status === 0 ? 'Pending' : day.ReClockIn.Status === 1 ? 'Approved' : 'Rejected'}`;
|
|
488
|
+
}
|
|
489
|
+
} else if (!day.ClockOut) {
|
|
490
|
+
status = 'No Out';
|
|
491
|
+
if (day.ReClockOut) {
|
|
492
|
+
notes = `ReClockOut: ${day.ReClockOut.Status === 0 ? 'Pending' : 'Done'}`;
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
status = 'OK';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Check for reclock info
|
|
499
|
+
if (day.ReClockIn && !notes) {
|
|
500
|
+
const reStatus = day.ReClockIn.Status === 0 ? 'Pending' : day.ReClockIn.Status === 1 ? 'Approved' : 'Rejected';
|
|
501
|
+
notes = `ReClockIn: ${reStatus}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(`║ ${date} ║ ${clockIn} ║ ${clockOut} ║ ${status.padEnd(8)} ║ ${notes.padEnd(21)} ║`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log('╚════════════╩══════════╩═══════════╩══════════╩═══════════════════════╝\n');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Do reclock request
|
|
511
|
+
function doReClock(jwt, date, time, isClockIn, location, reason) {
|
|
512
|
+
const body = JSON.stringify({
|
|
513
|
+
Date: date,
|
|
514
|
+
Time: time,
|
|
515
|
+
Location: location,
|
|
516
|
+
Reason: reason,
|
|
517
|
+
IsClockIn: isClockIn,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const cmd = `curl -s -X POST '${API_BASE}/Member/DoReClock' \
|
|
521
|
+
-H 'accept: text/plain' \
|
|
522
|
+
-H 'Authorization: Bearer Bearer ${jwt}' \
|
|
523
|
+
-H 'Content-Type: application/json-patch+json' \
|
|
524
|
+
-d '${body}'`;
|
|
525
|
+
|
|
526
|
+
const result = execSync(cmd, { encoding: 'utf-8' });
|
|
527
|
+
return JSON.parse(result);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Disable auto clock (remove cron jobs)
|
|
531
|
+
function disableAutoClock() {
|
|
532
|
+
console.log('\n========================================');
|
|
533
|
+
console.log(' Disable Auto Clock');
|
|
534
|
+
console.log('========================================\n');
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
// Get current crontab
|
|
538
|
+
const currentCron = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8' });
|
|
539
|
+
|
|
540
|
+
// Filter out auto-clock entries
|
|
541
|
+
const lines = currentCron.split('\n');
|
|
542
|
+
const filteredLines = lines.filter(line => !line.includes('auto-clock') && !line.includes('clock.js'));
|
|
543
|
+
|
|
544
|
+
if (lines.length === filteredLines.length) {
|
|
545
|
+
console.log('No auto-clock cron jobs found.\n');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Write new crontab
|
|
550
|
+
const newCron = filteredLines.join('\n');
|
|
551
|
+
execSync(`echo "${newCron}" | crontab -`, { encoding: 'utf-8' });
|
|
552
|
+
|
|
553
|
+
console.log('Auto clock cron jobs have been removed!');
|
|
554
|
+
console.log('You can re-enable by running: auto-clock cron\n');
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error('Failed to modify crontab:', error.message);
|
|
557
|
+
console.log('\nTo manually disable, run:');
|
|
558
|
+
console.log(' crontab -e');
|
|
559
|
+
console.log(' (remove lines containing "auto-clock" or "clock.js")\n');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Enable auto clock (add cron jobs with smart mode)
|
|
564
|
+
function enableAutoClock() {
|
|
565
|
+
const config = loadConfig();
|
|
566
|
+
if (!config) {
|
|
567
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const nodePath = process.execPath;
|
|
572
|
+
const scriptPath = path.join(__dirname, 'clock.js');
|
|
573
|
+
const [inHour] = config.clockInTime.split(':').map(Number);
|
|
574
|
+
const [outHour] = config.clockOutTime.split(':').map(Number);
|
|
575
|
+
|
|
576
|
+
// Fixed windows:
|
|
577
|
+
// - Clock In window: from clockInTime to 9:30 AM (hour 9)
|
|
578
|
+
// - Clock Out window: from clockOutTime to 6:00 PM (hour 18)
|
|
579
|
+
const inEndHour = 9; // 9:30 AM deadline
|
|
580
|
+
const outEndHour = 18; // 6:00 PM deadline
|
|
581
|
+
|
|
582
|
+
const clockInCron = `*/10 ${inHour}-${inEndHour} * * 1-5 ${nodePath} ${scriptPath} smart >> ${LOG_PATH} 2>&1`;
|
|
583
|
+
const clockOutCron = `*/10 ${outHour}-${outEndHour} * * 1-5 ${nodePath} ${scriptPath} smart >> ${LOG_PATH} 2>&1`;
|
|
584
|
+
|
|
585
|
+
console.log('\n========================================');
|
|
586
|
+
console.log(' Enable Smart Auto Clock');
|
|
587
|
+
console.log('========================================\n');
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
// Get current crontab
|
|
591
|
+
let currentCron = '';
|
|
592
|
+
try {
|
|
593
|
+
currentCron = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
594
|
+
} catch (e) {
|
|
595
|
+
// No existing crontab
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check if already exists
|
|
599
|
+
if (currentCron.includes('clock.js smart')) {
|
|
600
|
+
console.log('Smart auto clock is already enabled!\n');
|
|
601
|
+
console.log('Current settings:');
|
|
602
|
+
console.log(` Clock In Window: ${inHour}:00 - 9:30 AM (Mon-Fri)`);
|
|
603
|
+
console.log(` Clock Out Window: ${outHour}:00 - 6:00 PM (Mon-Fri)`);
|
|
604
|
+
console.log(` Check Interval: Every 10 minutes\n`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Remove old entries first
|
|
609
|
+
const lines = currentCron.split('\n').filter(line =>
|
|
610
|
+
!line.includes('auto-clock') && !line.includes('clock.js') && line.trim()
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// Add new smart entries for both windows
|
|
614
|
+
lines.push('# Auto Clock - Smart Mode (Clock In Window until 9:30 AM)');
|
|
615
|
+
lines.push(clockInCron);
|
|
616
|
+
lines.push('# Auto Clock - Smart Mode (Clock Out Window until 6 PM)');
|
|
617
|
+
lines.push(clockOutCron);
|
|
618
|
+
|
|
619
|
+
const newCron = lines.join('\n') + '\n';
|
|
620
|
+
execSync(`echo "${newCron}" | crontab -`, { encoding: 'utf-8' });
|
|
621
|
+
|
|
622
|
+
console.log('Smart auto clock has been enabled!');
|
|
623
|
+
console.log(`\nHow it works:`);
|
|
624
|
+
console.log(` - Clock In Window: ${inHour}:00 - 9:30 AM (every 10 min)`);
|
|
625
|
+
console.log(` - Clock Out Window: ${outHour}:00 - 6:00 PM (every 10 min)`);
|
|
626
|
+
console.log(` - Only runs Monday-Friday`);
|
|
627
|
+
console.log(`\nBenefit: Even if you open your laptop late, it will still clock you in!`);
|
|
628
|
+
console.log('\nTo disable: auto-clock disable\n');
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error('Failed to modify crontab:', error.message);
|
|
631
|
+
console.log('\nTo manually enable, run: auto-clock cron\n');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Smart auto clock - checks if clock in/out is needed
|
|
636
|
+
async function smartClock() {
|
|
637
|
+
const config = loadConfig();
|
|
638
|
+
if (!config) {
|
|
639
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Get fresh JWT
|
|
644
|
+
let jwt = config.jwt;
|
|
645
|
+
try {
|
|
646
|
+
const result = await login(config.email, config.password, true);
|
|
647
|
+
if (result.jwt) {
|
|
648
|
+
jwt = result.jwt;
|
|
649
|
+
config.jwt = jwt;
|
|
650
|
+
saveConfig(config);
|
|
651
|
+
}
|
|
652
|
+
} catch (e) {
|
|
653
|
+
// Use existing token
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!jwt) {
|
|
657
|
+
log('Smart Clock: No JWT token');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Get today's status
|
|
662
|
+
const data = getHistory(jwt, 1);
|
|
663
|
+
if (data.ErrorCode !== 0) {
|
|
664
|
+
log(`Smart Clock: Failed to get status - ${data.ErrorMessage}`);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const now = new Date();
|
|
669
|
+
const currentHour = now.getHours();
|
|
670
|
+
const currentMin = now.getMinutes();
|
|
671
|
+
const currentTime = currentHour * 60 + currentMin;
|
|
672
|
+
const dayOfWeek = now.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
|
|
673
|
+
|
|
674
|
+
// Only run Mon-Fri
|
|
675
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const [inHour, inMin] = config.clockInTime.split(':').map(Number);
|
|
680
|
+
const [outHour, outMin] = config.clockOutTime.split(':').map(Number);
|
|
681
|
+
const clockInTime = inHour * 60 + inMin;
|
|
682
|
+
const clockOutTime = outHour * 60 + outMin;
|
|
683
|
+
|
|
684
|
+
// Find today's record
|
|
685
|
+
const today = now.toISOString().split('T')[0];
|
|
686
|
+
const todayRecord = data.Result.find(d => d.WorkDate === today);
|
|
687
|
+
|
|
688
|
+
const hasClockIn = todayRecord?.ClockIn;
|
|
689
|
+
const hasClockOut = todayRecord?.ClockOut;
|
|
690
|
+
|
|
691
|
+
// Check if we should clock in
|
|
692
|
+
// Clock in window: from clockInTime to clockOutTime, if not already clocked in
|
|
693
|
+
if (!hasClockIn && currentTime >= clockInTime && currentTime < clockOutTime) {
|
|
694
|
+
log(`Smart Clock: Auto clocking in (opened late at ${String(currentHour).padStart(2, '0')}:${String(currentMin).padStart(2, '0')})`);
|
|
695
|
+
const result = doClock(jwt, true, config.location || 'TechBodia');
|
|
696
|
+
if (result.success) {
|
|
697
|
+
log(`Smart Clock: Clock In successful`);
|
|
698
|
+
} else if (result.alreadyDone) {
|
|
699
|
+
log(`Smart Clock: ${result.message}`);
|
|
700
|
+
} else {
|
|
701
|
+
log(`Smart Clock: Clock In failed - ${result.message}`);
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Check if we should clock out
|
|
707
|
+
// Clock out window: after clockOutTime, if clocked in but not out
|
|
708
|
+
if (hasClockIn && !hasClockOut && currentTime >= clockOutTime) {
|
|
709
|
+
log(`Smart Clock: Auto clocking out (time: ${String(currentHour).padStart(2, '0')}:${String(currentMin).padStart(2, '0')})`);
|
|
710
|
+
const result = doClock(jwt, false, config.location || 'TechBodia');
|
|
711
|
+
if (result.success) {
|
|
712
|
+
log(`Smart Clock: Clock Out successful`);
|
|
713
|
+
} else if (result.alreadyDone) {
|
|
714
|
+
log(`Smart Clock: ${result.message}`);
|
|
715
|
+
} else {
|
|
716
|
+
log(`Smart Clock: Clock Out failed - ${result.message}`);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Reclock command
|
|
723
|
+
async function reclock() {
|
|
724
|
+
const config = loadConfig();
|
|
725
|
+
if (!config) {
|
|
726
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Try to get fresh JWT
|
|
731
|
+
let jwt = config.jwt;
|
|
732
|
+
try {
|
|
733
|
+
const result = await login(config.email, config.password, true);
|
|
734
|
+
if (result.jwt) {
|
|
735
|
+
jwt = result.jwt;
|
|
736
|
+
config.jwt = jwt;
|
|
737
|
+
saveConfig(config);
|
|
738
|
+
}
|
|
739
|
+
} catch (e) {
|
|
740
|
+
// Use existing token
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!jwt) {
|
|
744
|
+
console.error('No JWT token. Run "auto-clock token" to set one.');
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Calculate defaults
|
|
749
|
+
const today = new Date();
|
|
750
|
+
const defaultDate = today.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
751
|
+
const defaultInTime = config.clockInTime || '08:30';
|
|
752
|
+
const defaultOutTime = config.clockOutTime || '17:30';
|
|
753
|
+
|
|
754
|
+
console.log('\n========================================');
|
|
755
|
+
console.log(' Request ReClock');
|
|
756
|
+
console.log('========================================\n');
|
|
757
|
+
console.log('Press Enter to use default values shown in brackets.\n');
|
|
758
|
+
|
|
759
|
+
const dateStr = (await prompt(`Date [${defaultDate}]: `)) || defaultDate;
|
|
760
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
761
|
+
console.error('Invalid date format. Use YYYY-MM-DD (e.g., 2025-01-10)\n');
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const typeInput = (await prompt('Type [in]/out: ')) || 'in';
|
|
766
|
+
if (!['in', 'out'].includes(typeInput.toLowerCase())) {
|
|
767
|
+
console.error('Invalid type. Use "in" or "out"\n');
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const isClockIn = typeInput.toLowerCase() === 'in';
|
|
772
|
+
const defaultTime = isClockIn ? defaultInTime : defaultOutTime;
|
|
773
|
+
|
|
774
|
+
const time = (await prompt(`Time [${defaultTime}]: `)) || defaultTime;
|
|
775
|
+
if (!/^\d{2}:\d{2}$/.test(time)) {
|
|
776
|
+
console.error('Invalid time format. Use HH:MM (e.g., 08:30)\n');
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const reason = (await prompt('Reason [Forgot to clock]: ')) || 'Forgot to clock';
|
|
781
|
+
|
|
782
|
+
// Date format: YYYY-MM-DDT00:00:00+07:00 (use midnight for the date part)
|
|
783
|
+
const date = `${dateStr}T00:00:00+07:00`;
|
|
784
|
+
|
|
785
|
+
console.log('\nSubmitting reclock request...');
|
|
786
|
+
console.log(` Date: ${dateStr}`);
|
|
787
|
+
console.log(` Time: ${time}`);
|
|
788
|
+
console.log(` Type: ${isClockIn ? 'Clock In' : 'Clock Out'}`);
|
|
789
|
+
console.log(` Reason: ${reason}`);
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const result = doReClock(jwt, date, time, isClockIn, config.location || 'TechBodia', reason);
|
|
793
|
+
|
|
794
|
+
if (result.ErrorCode === 0) {
|
|
795
|
+
console.log(`\nReClock submitted successfully!`);
|
|
796
|
+
console.log(`Request ID: ${result.Result.RequestId}\n`);
|
|
797
|
+
} else {
|
|
798
|
+
console.error(`\nReClock failed: ${result.ErrorMessage || JSON.stringify(result)}\n`);
|
|
799
|
+
}
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error(`\nReClock failed: ${error.message}\n`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Reclock for a date range
|
|
806
|
+
async function reclockRange() {
|
|
807
|
+
const config = loadConfig();
|
|
808
|
+
if (!config) {
|
|
809
|
+
console.error('Not configured. Run "auto-clock setup" first.');
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Try to get fresh JWT
|
|
814
|
+
let jwt = config.jwt;
|
|
815
|
+
try {
|
|
816
|
+
const result = await login(config.email, config.password, true);
|
|
817
|
+
if (result.jwt) {
|
|
818
|
+
jwt = result.jwt;
|
|
819
|
+
config.jwt = jwt;
|
|
820
|
+
saveConfig(config);
|
|
821
|
+
}
|
|
822
|
+
} catch (e) {
|
|
823
|
+
// Use existing token
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!jwt) {
|
|
827
|
+
console.error('No JWT token. Run "auto-clock token" to set one.');
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Calculate defaults
|
|
832
|
+
const today = new Date();
|
|
833
|
+
const defaultEndDate = today.toISOString().split('T')[0];
|
|
834
|
+
const thirtyDaysAgo = new Date(today);
|
|
835
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
836
|
+
const defaultStartDate = thirtyDaysAgo.toISOString().split('T')[0];
|
|
837
|
+
const defaultInTime = config.clockInTime || '08:30';
|
|
838
|
+
const defaultOutTime = config.clockOutTime || '17:30';
|
|
839
|
+
|
|
840
|
+
console.log('\n========================================');
|
|
841
|
+
console.log(' ReClock Range (Multiple Days)');
|
|
842
|
+
console.log('========================================\n');
|
|
843
|
+
console.log('This will submit reclock for each weekday in the date range.');
|
|
844
|
+
console.log('Press Enter to use default values shown in brackets.\n');
|
|
845
|
+
|
|
846
|
+
const startDateStr = (await prompt(`Start Date [${defaultStartDate}]: `)) || defaultStartDate;
|
|
847
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) {
|
|
848
|
+
console.error('Invalid date format. Use YYYY-MM-DD\n');
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const endDateStr = (await prompt(`End Date [${defaultEndDate}]: `)) || defaultEndDate;
|
|
853
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(endDateStr)) {
|
|
854
|
+
console.error('Invalid date format. Use YYYY-MM-DD\n');
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const typeInput = (await prompt('Type [in]/out/both: ')) || 'in';
|
|
859
|
+
if (!['in', 'out', 'both'].includes(typeInput.toLowerCase())) {
|
|
860
|
+
console.error('Invalid type. Use "in", "out", or "both"\n');
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const doBoth = typeInput.toLowerCase() === 'both';
|
|
865
|
+
const doClockIn = doBoth || typeInput.toLowerCase() === 'in';
|
|
866
|
+
const doClockOut = doBoth || typeInput.toLowerCase() === 'out';
|
|
867
|
+
|
|
868
|
+
let inTime = defaultInTime;
|
|
869
|
+
let outTime = defaultOutTime;
|
|
870
|
+
|
|
871
|
+
if (doClockIn) {
|
|
872
|
+
inTime = (await prompt(`Clock In Time [${defaultInTime}]: `)) || defaultInTime;
|
|
873
|
+
if (!/^\d{2}:\d{2}$/.test(inTime)) {
|
|
874
|
+
console.error('Invalid time format. Use HH:MM\n');
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (doClockOut) {
|
|
880
|
+
outTime = (await prompt(`Clock Out Time [${defaultOutTime}]: `)) || defaultOutTime;
|
|
881
|
+
if (!/^\d{2}:\d{2}$/.test(outTime)) {
|
|
882
|
+
console.error('Invalid time format. Use HH:MM\n');
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const reason = (await prompt('Reason [Forgot to clock]: ')) || 'Forgot to clock';
|
|
888
|
+
|
|
889
|
+
// Generate list of weekdays in range
|
|
890
|
+
const startDate = new Date(startDateStr);
|
|
891
|
+
const endDate = new Date(endDateStr);
|
|
892
|
+
const dates = [];
|
|
893
|
+
|
|
894
|
+
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
|
895
|
+
const dayOfWeek = d.getDay();
|
|
896
|
+
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Skip weekends
|
|
897
|
+
dates.push(d.toISOString().split('T')[0]);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (dates.length === 0) {
|
|
902
|
+
console.log('\nNo weekdays found in the specified range.\n');
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
console.log(`\nFound ${dates.length} weekday(s) to reclock:`);
|
|
907
|
+
console.log(` From: ${startDateStr}`);
|
|
908
|
+
console.log(` To: ${endDateStr}`);
|
|
909
|
+
if (doClockIn) console.log(` Clock In: ${inTime}`);
|
|
910
|
+
if (doClockOut) console.log(` Clock Out: ${outTime}`);
|
|
911
|
+
console.log(` Reason: ${reason}`);
|
|
912
|
+
|
|
913
|
+
const confirm = await prompt('\nProceed? (y/n) [y]: ');
|
|
914
|
+
if (confirm.toLowerCase() === 'n') {
|
|
915
|
+
console.log('Cancelled.\n');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
console.log('\nSubmitting reclock requests...\n');
|
|
920
|
+
|
|
921
|
+
let successCount = 0;
|
|
922
|
+
let failCount = 0;
|
|
923
|
+
|
|
924
|
+
for (const dateStr of dates) {
|
|
925
|
+
const date = `${dateStr}T00:00:00+07:00`;
|
|
926
|
+
|
|
927
|
+
if (doClockIn) {
|
|
928
|
+
try {
|
|
929
|
+
const result = doReClock(jwt, date, inTime, true, config.location || 'TechBodia', reason);
|
|
930
|
+
if (result.ErrorCode === 0) {
|
|
931
|
+
console.log(` ${dateStr} IN - OK (ID: ${result.Result.RequestId})`);
|
|
932
|
+
successCount++;
|
|
933
|
+
} else {
|
|
934
|
+
console.log(` ${dateStr} IN - FAILED: ${result.ErrorMessage || 'Unknown error'}`);
|
|
935
|
+
failCount++;
|
|
936
|
+
}
|
|
937
|
+
} catch (error) {
|
|
938
|
+
console.log(` ${dateStr} IN - FAILED: ${error.message}`);
|
|
939
|
+
failCount++;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (doClockOut) {
|
|
944
|
+
try {
|
|
945
|
+
const result = doReClock(jwt, date, outTime, false, config.location || 'TechBodia', reason);
|
|
946
|
+
if (result.ErrorCode === 0) {
|
|
947
|
+
console.log(` ${dateStr} OUT - OK (ID: ${result.Result.RequestId})`);
|
|
948
|
+
successCount++;
|
|
949
|
+
} else {
|
|
950
|
+
console.log(` ${dateStr} OUT - FAILED: ${result.ErrorMessage || 'Unknown error'}`);
|
|
951
|
+
failCount++;
|
|
952
|
+
}
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.log(` ${dateStr} OUT - FAILED: ${error.message}`);
|
|
955
|
+
failCount++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Small delay to avoid rate limiting
|
|
960
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
console.log(`\nCompleted: ${successCount} success, ${failCount} failed\n`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Main CLI
|
|
967
|
+
async function main() {
|
|
968
|
+
const command = process.argv[2];
|
|
969
|
+
|
|
970
|
+
switch (command) {
|
|
971
|
+
case 'setup':
|
|
972
|
+
await setup();
|
|
973
|
+
break;
|
|
974
|
+
|
|
975
|
+
case 'token':
|
|
976
|
+
await setToken();
|
|
977
|
+
break;
|
|
978
|
+
|
|
979
|
+
case 'in': {
|
|
980
|
+
const inSuccess = await performClock(true);
|
|
981
|
+
process.exit(inSuccess ? 0 : 1);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
case 'out': {
|
|
985
|
+
const outSuccess = await performClock(false);
|
|
986
|
+
process.exit(outSuccess ? 0 : 1);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
case 'schedule':
|
|
990
|
+
await setSchedule();
|
|
991
|
+
break;
|
|
992
|
+
|
|
993
|
+
case 'cron':
|
|
994
|
+
showCron();
|
|
995
|
+
break;
|
|
996
|
+
|
|
997
|
+
case 'status':
|
|
998
|
+
showStatus();
|
|
999
|
+
break;
|
|
1000
|
+
|
|
1001
|
+
case 'logs':
|
|
1002
|
+
showLogs();
|
|
1003
|
+
break;
|
|
1004
|
+
|
|
1005
|
+
case 'history': {
|
|
1006
|
+
const days = parseInt(process.argv[3]) || 14;
|
|
1007
|
+
await showHistory(days);
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
case 'reclock':
|
|
1012
|
+
await reclock();
|
|
1013
|
+
break;
|
|
1014
|
+
|
|
1015
|
+
case 'reclock-range':
|
|
1016
|
+
await reclockRange();
|
|
1017
|
+
break;
|
|
1018
|
+
|
|
1019
|
+
case 'enable':
|
|
1020
|
+
enableAutoClock();
|
|
1021
|
+
break;
|
|
1022
|
+
|
|
1023
|
+
case 'disable':
|
|
1024
|
+
disableAutoClock();
|
|
1025
|
+
break;
|
|
1026
|
+
|
|
1027
|
+
case 'smart':
|
|
1028
|
+
case 'auto':
|
|
1029
|
+
await smartClock();
|
|
1030
|
+
break;
|
|
1031
|
+
|
|
1032
|
+
case 'help':
|
|
1033
|
+
case '--help':
|
|
1034
|
+
case '-h':
|
|
1035
|
+
showHelp();
|
|
1036
|
+
break;
|
|
1037
|
+
|
|
1038
|
+
default:
|
|
1039
|
+
if (command) {
|
|
1040
|
+
console.error(`Unknown command: ${command}\n`);
|
|
1041
|
+
}
|
|
1042
|
+
showHelp();
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
main().catch((err) => {
|
|
1048
|
+
console.error('Error:', err.message);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auto-clock-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for automatic clock in/out with HRMS integration",
|
|
5
|
+
"main": "clock.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"auto-clock": "./clock.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node clock.js help"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"clock",
|
|
14
|
+
"attendance",
|
|
15
|
+
"hrms",
|
|
16
|
+
"cli",
|
|
17
|
+
"auto-clock",
|
|
18
|
+
"clock-in",
|
|
19
|
+
"clock-out",
|
|
20
|
+
"time-tracking"
|
|
21
|
+
],
|
|
22
|
+
"author": "TechBodia",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/techbodia/auto-clock.git"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"clock.js",
|
|
33
|
+
"README.md",
|
|
34
|
+
"config.example.json"
|
|
35
|
+
]
|
|
36
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Auto Clock CLI
|
|
2
|
+
|
|
3
|
+
A CLI tool to automatically clock in and clock out at scheduled times.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Option 1: Install globally from npm (for team members)
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g auto-clock
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Option 2: Install from local folder
|
|
13
|
+
```bash
|
|
14
|
+
cd /path/to/auto-clock
|
|
15
|
+
npm install -g .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Option 3: Run directly without installing
|
|
19
|
+
```bash
|
|
20
|
+
node /path/to/auto-clock/clock.js <command>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Setup (first-time users)
|
|
26
|
+
```bash
|
|
27
|
+
auto-clock setup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This will prompt you for:
|
|
31
|
+
- Email
|
|
32
|
+
- Password
|
|
33
|
+
- Location (default: TechBodia)
|
|
34
|
+
- Clock in time (e.g., 08:00)
|
|
35
|
+
- Clock out time (e.g., 17:00)
|
|
36
|
+
|
|
37
|
+
### 2. Test manually
|
|
38
|
+
```bash
|
|
39
|
+
auto-clock in # Clock in now
|
|
40
|
+
auto-clock out # Clock out now
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Set up automatic scheduling
|
|
44
|
+
```bash
|
|
45
|
+
auto-clock cron # Shows cron commands to copy
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---------|-------------|
|
|
52
|
+
| `auto-clock setup` | Interactive setup wizard |
|
|
53
|
+
| `auto-clock in` | Clock in now |
|
|
54
|
+
| `auto-clock out` | Clock out now |
|
|
55
|
+
| `auto-clock schedule` | Change clock in/out times |
|
|
56
|
+
| `auto-clock cron` | Show cron setup instructions |
|
|
57
|
+
| `auto-clock status` | Show current configuration |
|
|
58
|
+
| `auto-clock logs` | Show recent logs |
|
|
59
|
+
| `auto-clock help` | Show help |
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
Config is stored at: `~/.auto-clock/config.json`
|
|
64
|
+
Logs are stored at: `~/.auto-clock/clock.log`
|
|
65
|
+
|
|
66
|
+
## Scheduling with Cron (macOS/Linux)
|
|
67
|
+
|
|
68
|
+
After running `auto-clock cron`, copy the displayed commands to your crontab:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
crontab -e
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example cron entries (8:00 AM clock in, 5:00 PM clock out, Mon-Fri):
|
|
75
|
+
```cron
|
|
76
|
+
0 8 * * 1-5 /path/to/node /path/to/clock.js in >> ~/.auto-clock/clock.log 2>&1
|
|
77
|
+
0 17 * * 1-5 /path/to/node /path/to/clock.js out >> ~/.auto-clock/clock.log 2>&1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Note:** Your computer must be ON and awake at scheduled times.
|
|
81
|
+
|
|
82
|
+
## For Multiple Users
|
|
83
|
+
|
|
84
|
+
Each user:
|
|
85
|
+
1. Installs the tool: `npm install -g auto-clock`
|
|
86
|
+
2. Runs setup: `auto-clock setup`
|
|
87
|
+
3. Configures cron: `auto-clock cron`
|
|
88
|
+
|
|
89
|
+
Each user's config is stored separately in their home directory.
|
|
90
|
+
|
|
91
|
+
## Uninstall
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm uninstall -g auto-clock
|
|
95
|
+
rm -rf ~/.auto-clock
|
|
96
|
+
```
|