OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana

Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
oxpoe committed 2026-10-09 05:51:21 +07:00
commit eaf05f2811
280 files changed
+123975

No files matched your search

+318
View File
@@ -0,0 +1,318 @@
const db = require('../config/database');
const { getSetting, getCurrentDateInTimezone, parseDateInTimezone } = require('../config/settingsManager');
/**
* ATTENDANCE SERVICE
* Mengelola absensi karyawan (teknisi, admin, cashier, collector)
*/
// Calculate distance between two coordinates (Haversine formula)
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Radius of Earth in kilometers
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const distance = R * c * 1000; // Convert to meters
return distance;
}
// Validate if location is within allowed radius
function validateLocation(lat, lng) {
// Get office location from settings
const officeLat = parseFloat(getSetting('office_lat', '0'));
const officeLng = parseFloat(getSetting('office_lng', '0'));
const allowedRadius = parseInt(getSetting('attendance_radius', '100')); // Default 100 meters
const geofencingEnabled = getSetting('attendance_geofencing', 'true') === 'true';
// If geofencing disabled or office location not set, allow all
if (!geofencingEnabled || !officeLat || !officeLng) {
return { valid: true, distance: 0, message: 'Geofencing disabled' };
}
// If no GPS provided
if (!lat || !lng) {
return { valid: false, distance: 0, message: 'GPS location required' };
}
// Calculate distance
const distance = calculateDistance(officeLat, officeLng, parseFloat(lat), parseFloat(lng));
// Check if within radius
if (distance <= allowedRadius) {
return {
valid: true,
distance: Math.round(distance),
message: `Within range (${Math.round(distance)}m from office)`
};
} else {
return {
valid: false,
distance: Math.round(distance),
message: `Too far from office (${Math.round(distance)}m, max ${allowedRadius}m)`
};
}
}
// Create attendance record (check-in)
function checkIn(data) {
// Validate location
const locationCheck = validateLocation(data.lat, data.lng);
if (!locationCheck.valid) {
throw new Error(locationCheck.message);
}
const stmt = db.prepare(`
INSERT INTO attendance (
employee_type, employee_id, employee_name,
check_in_time, check_in_lat, check_in_lng, check_in_note, check_in_photo
) VALUES (?, ?, ?, NOW_LOCAL(), ?, ?, ?, ?)
`);
return stmt.run(
data.employee_type,
data.employee_id,
data.employee_name,
data.lat || '',
data.lng || '',
data.note || '',
data.photo || ''
);
}
// Update attendance record (check-out)
function checkOut(attendanceId, data) {
const attendance = db.prepare('SELECT * FROM attendance WHERE id = ?').get(attendanceId);
if (!attendance) {
throw new Error('Attendance record not found');
}
if (attendance.status === 'checked_out') {
throw new Error('Already checked out');
}
// Validate location for check-out
const locationCheck = validateLocation(data.lat, data.lng);
if (!locationCheck.valid) {
throw new Error(locationCheck.message);
}
// Calculate work duration in minutes
const checkInTime = parseDateInTimezone(attendance.check_in_time);
const checkOutTime = new Date();
const durationMinutes = Math.floor((checkOutTime - checkInTime) / 1000 / 60);
const stmt = db.prepare(`
UPDATE attendance
SET check_out_time = NOW_LOCAL(),
check_out_lat = ?,
check_out_lng = ?,
check_out_note = ?,
check_out_photo = ?,
work_duration_minutes = ?,
status = 'checked_out'
WHERE id = ?
`);
return stmt.run(
data.lat || '',
data.lng || '',
data.note || '',
data.photo || '',
durationMinutes,
attendanceId
);
}
// Get today's attendance for an employee
function getTodayAttendance(employeeType, employeeId) {
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE employee_type = ?
AND employee_id = ?
AND date(check_in_time) = date(NOW_LOCAL())
ORDER BY check_in_time DESC
LIMIT 1
`);
return stmt.get(employeeType, employeeId);
}
// Get attendance history for an employee
function getAttendanceHistory(employeeType, employeeId, limit = 30) {
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE employee_type = ? AND employee_id = ?
ORDER BY check_in_time DESC
LIMIT ?
`);
return stmt.all(employeeType, employeeId, limit);
}
// Get all attendance records for a specific date
function getAttendanceByDate(date) {
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE date(check_in_time) = date(?)
ORDER BY check_in_time DESC
`);
return stmt.all(date);
}
// Get attendance records for a date range
function getAttendanceByDateRange(startDate, endDate) {
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE date(check_in_time) BETWEEN date(?) AND date(?)
ORDER BY check_in_time DESC
`);
return stmt.all(startDate, endDate);
}
// Get attendance summary for an employee (monthly)
function getMonthlyAttendanceSummary(employeeType, employeeId, year, month) {
const stmt = db.prepare(`
SELECT
COUNT(*) as total_days,
SUM(CASE WHEN status = 'checked_out' THEN 1 ELSE 0 END) as completed_days,
SUM(work_duration_minutes) as total_minutes,
AVG(work_duration_minutes) as avg_minutes
FROM attendance
WHERE employee_type = ?
AND employee_id = ?
AND strftime('%Y', check_in_time) = ?
AND strftime('%m', check_in_time) = ?
`);
const yearStr = String(year);
const monthStr = String(month).padStart(2, '0');
return stmt.get(employeeType, employeeId, yearStr, monthStr);
}
// Get all attendance for today (for admin dashboard)
function getTodayAllAttendance() {
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE date(check_in_time) = date(NOW_LOCAL())
ORDER BY check_in_time DESC
`);
return stmt.all();
}
// Get attendance statistics for admin
function getAttendanceStats(date = null) {
const dateFilter = date ? `date(check_in_time) = date('${date}')` : `date(check_in_time) = date(NOW_LOCAL())`;
const stmt = db.prepare(`
SELECT
employee_type,
COUNT(*) as total,
SUM(CASE WHEN status = 'checked_in' THEN 1 ELSE 0 END) as checked_in,
SUM(CASE WHEN status = 'checked_out' THEN 1 ELSE 0 END) as checked_out
FROM attendance
WHERE ${dateFilter}
GROUP BY employee_type
`);
return stmt.all();
}
// Check if employee has checked in today
function hasCheckedInToday(employeeType, employeeId) {
const today = getTodayAttendance(employeeType, employeeId);
return today !== undefined;
}
// Get late check-ins (after 8:30 AM)
function getLateCheckIns(date = null) {
const dateFilter = date ? `date(check_in_time) = date('${date}')` : `date(check_in_time) = date(NOW_LOCAL())`;
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE ${dateFilter}
AND time(check_in_time) > '08:30:00'
ORDER BY check_in_time DESC
`);
return stmt.all();
}
// Get employees who haven't checked out
function getNotCheckedOut(date = null) {
const dateFilter = date ? `date(check_in_time) = date('${date}')` : `date(check_in_time) = date(NOW_LOCAL())`;
const stmt = db.prepare(`
SELECT * FROM attendance
WHERE ${dateFilter}
AND status = 'checked_in'
ORDER BY check_in_time DESC
`);
return stmt.all();
}
// Delete attendance record (admin only)
function deleteAttendance(id) {
return db.prepare('DELETE FROM attendance WHERE id = ?').run(id);
}
// Update attendance record (admin only - for corrections)
function updateAttendance(id, data) {
const stmt = db.prepare(`
UPDATE attendance
SET check_in_time = ?,
check_in_note = ?,
check_out_time = ?,
check_out_note = ?,
work_duration_minutes = ?
WHERE id = ?
`);
return stmt.run(
data.check_in_time,
data.check_in_note || '',
data.check_out_time || null,
data.check_out_note || '',
data.work_duration_minutes || 0,
id
);
}
// Get geofencing settings
function getGeofencingSettings() {
return {
enabled: getSetting('attendance_geofencing', 'true') === 'true',
officeLat: parseFloat(getSetting('office_lat', '0')),
officeLng: parseFloat(getSetting('office_lng', '0')),
radius: parseInt(getSetting('attendance_radius', '100'))
};
}
module.exports = {
checkIn,
checkOut,
getTodayAttendance,
getAttendanceHistory,
getAttendanceByDate,
getAttendanceByDateRange,
getMonthlyAttendanceSummary,
getTodayAllAttendance,
getAttendanceStats,
hasCheckedInToday,
getLateCheckIns,
getNotCheckedOut,
deleteAttendance,
updateAttendance,
validateLocation,
calculateDistance,
getGeofencingSettings
};
// Made with Bob