feat(地理围栏): 添加地理围栏功能并优化打卡记录处理
- 添加@turf/turf依赖用于地理围栏计算 - 实现地理围栏检查,拒绝围栏外的打卡并记录失败事件 - 过滤掉失败事件在工人历史记录中显示 - 修复地图链接中的错误语法 - 移除开发提示和HTTPS相关代码 - 优化视图按钮样式和事件类型颜色标识
This commit is contained in:
+45
-4
@@ -1,5 +1,3 @@
|
||||
import https from 'https'
|
||||
import fs from 'fs'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import { Parser } from 'json2csv'
|
||||
@@ -8,6 +6,11 @@ import mysql from 'mysql2/promise'
|
||||
import dotenv from 'dotenv'
|
||||
import bcrypt from 'bcrypt'
|
||||
import jwt from 'jsonwebtoken'
|
||||
// --- FIX START ---
|
||||
// Import only the required functions from turf
|
||||
import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf'
|
||||
// --- FIX END ---
|
||||
|
||||
|
||||
// Main function to start the server
|
||||
async function startServer() {
|
||||
@@ -38,6 +41,22 @@ async function startServer() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// --- FIX START ---
|
||||
// Define the geofence polygon by calling the 'polygon' function directly
|
||||
const geofence = polygon([
|
||||
[
|
||||
[101.80827335908509, 2.8350045747358337],
|
||||
[101.80822799653066, 2.8340134829130363],
|
||||
[101.80827902940462, 2.8335264317641418],
|
||||
[101.80941309326164, 2.8332772427247335],
|
||||
[101.81144873788423, 2.834596811345506],
|
||||
[101.81166988033686, 2.8345911479647157],
|
||||
[101.81199875885511, 2.83593336858695],
|
||||
[101.80827335908509, 2.8350045747358337],
|
||||
],
|
||||
])
|
||||
// --- FIX END ---
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
@@ -96,6 +115,28 @@ async function startServer() {
|
||||
try {
|
||||
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
|
||||
|
||||
// Geofencing check using the directly imported functions
|
||||
const userLocation = point([longitude, latitude]);
|
||||
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
|
||||
|
||||
if (!isWithinGeofence) {
|
||||
// User is outside the geofence, log a 'failed' attempt
|
||||
// Calculate the distance from the geofence
|
||||
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
|
||||
// Create a descriptive note
|
||||
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
|
||||
|
||||
// Insert the failed attempt into the database
|
||||
await db.execute(
|
||||
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes, distance_meters) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[userId, 'failed', new Date(), qrCodeValue, latitude, longitude, notes, distance]
|
||||
);
|
||||
|
||||
// Return an error to the user
|
||||
return res.status(403).json({ message: `You are not within the allowed work area.` });
|
||||
// --- MODIFICATION END ---
|
||||
}
|
||||
|
||||
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
|
||||
qrCodeValue,
|
||||
])
|
||||
@@ -536,7 +577,7 @@ async function startServer() {
|
||||
const placeholders = idsArray.map(() => '?').join(',')
|
||||
|
||||
// MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries, and select `notes`
|
||||
let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})`
|
||||
let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes, cr.distance_meters FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})`;
|
||||
|
||||
const params = [...idsArray]
|
||||
if (startDate && endDate) {
|
||||
@@ -657,7 +698,7 @@ async function startServer() {
|
||||
// }
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on https://localhost:${port}`)
|
||||
console.log(`Server is running on http://localhost:${port}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Generated
+2253
-1
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
"@capacitor/cli": "^7.4.0",
|
||||
"@capacitor/core": "^7.4.0",
|
||||
"@primeuix/themes": "^1.1.2",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
</button>
|
||||
<button
|
||||
@click="viewRecords(worker.id)"
|
||||
class="bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-white px-3 py-1 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-md text-sm font-medium transition-colors duration-200"
|
||||
>
|
||||
View Records
|
||||
</button>
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
:class="{
|
||||
'bg-green-500': record.event_type === 'clock_in',
|
||||
'bg-red-500': record.event_type === 'clock_out',
|
||||
'bg-yellow-500': record.event_type === 'failed',
|
||||
}"
|
||||
>{{ record.event_type.replace('_', ' ') }}</span
|
||||
>
|
||||
@@ -146,7 +147,7 @@
|
||||
<td class="px-4 py-3">
|
||||
<a
|
||||
v-if="record.latitude && record.longitude"
|
||||
:href="`https://maps.google.com/?q=$${record.latitude},${record.longitude}`"
|
||||
:href="`https://maps.google.com/?q=${record.latitude},${record.longitude}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 hover:text-blue-800 underline font-medium"
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 text-center mb-6">
|
||||
<!-- <p class="text-xs text-gray-500 dark:text-gray-400 text-center mb-6">
|
||||
Hint: worker/password or manager/password
|
||||
</p>
|
||||
</p> -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-md text-lg transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
|
||||
@@ -56,7 +56,7 @@ onMounted(async () => {
|
||||
try {
|
||||
const data = await apiFetch(`/api/worker/clock-history/${userId}`)
|
||||
if (data) {
|
||||
clockHistory.value = data
|
||||
clockHistory.value = data.filter(event => event.event_type !== 'failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch clock history:', error)
|
||||
|
||||
Reference in New Issue
Block a user