code changes
This commit is contained in:
@@ -4774,7 +4774,10 @@ def attendance_report():
|
|||||||
ad.device_info,
|
ad.device_info,
|
||||||
ad.created_timestamp,
|
ad.created_timestamp,
|
||||||
ad.updated_timestamp,
|
ad.updated_timestamp,
|
||||||
CONCAT(e.firstName, ' ', e.lastName) as employee_name
|
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
||||||
|
ad.verification_required,
|
||||||
|
ad.verification_status,
|
||||||
|
ad.verification_photo
|
||||||
FROM attendance_data ad
|
FROM attendance_data ad
|
||||||
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
||||||
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
@@ -4799,7 +4802,10 @@ def attendance_report():
|
|||||||
ad.device_info,
|
ad.device_info,
|
||||||
ad.created_timestamp,
|
ad.created_timestamp,
|
||||||
ad.updated_timestamp,
|
ad.updated_timestamp,
|
||||||
CONCAT(e.firstName, ' ', e.lastName) as employee_name
|
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
||||||
|
ad.verification_required,
|
||||||
|
ad.verification_status,
|
||||||
|
ad.verification_photo
|
||||||
FROM attendance_data ad
|
FROM attendance_data ad
|
||||||
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
||||||
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
@@ -4885,8 +4891,21 @@ def attendance_report():
|
|||||||
'device_info': record[12],
|
'device_info': record[12],
|
||||||
'created_timestamp': record[13],
|
'created_timestamp': record[13],
|
||||||
'updated_timestamp': record[14],
|
'updated_timestamp': record[14],
|
||||||
'employee_name': record[15] or 'Unknown Employee'
|
'employee_name': record[15] or 'Unknown Employee',
|
||||||
|
'verification_required': record[16] if len(record) > 16 else False,
|
||||||
|
'verification_status': record[17] if len(record) > 17 else None,
|
||||||
|
'verification_photo': record[18] if len(record) > 18 else None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Calculate accuracy_level for template display
|
||||||
|
if record_dict['location_accuracy'] is not None:
|
||||||
|
accuracy_value = float(record_dict['location_accuracy'])
|
||||||
|
if accuracy_value <= 0.3:
|
||||||
|
record_dict['accuracy_level'] = 'accurate'
|
||||||
|
else:
|
||||||
|
record_dict['accuracy_level'] = 'inaccurate'
|
||||||
|
else:
|
||||||
|
record_dict['accuracy_level'] = 'unknown'
|
||||||
processed_records.append(record_dict)
|
processed_records.append(record_dict)
|
||||||
except Exception as rec_error:
|
except Exception as rec_error:
|
||||||
print(f"⚠️ Error processing record: {rec_error}")
|
print(f"⚠️ Error processing record: {rec_error}")
|
||||||
@@ -5406,6 +5425,91 @@ def update_verification_status(record_id):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'message': 'Error updating verification status'
|
'message': 'Error updating verification status'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/attendance/<int:record_id>/verification-details')
|
||||||
|
@login_required
|
||||||
|
def get_verification_details(record_id):
|
||||||
|
"""API endpoint to get verification details for a specific record"""
|
||||||
|
try:
|
||||||
|
# Get the attendance record with verification data
|
||||||
|
record = AttendanceData.query.get_or_404(record_id)
|
||||||
|
|
||||||
|
# DEBUG: Log record details
|
||||||
|
print(f"=== VERIFICATION DETAILS DEBUG ===")
|
||||||
|
print(f"Record ID: {record.id}")
|
||||||
|
print(f"Employee: {record.employee_id}")
|
||||||
|
print(f"check_in_date type: {type(record.check_in_date)}")
|
||||||
|
print(f"check_in_date value: {record.check_in_date}")
|
||||||
|
print(f"check_in_time type: {type(record.check_in_time)}")
|
||||||
|
print(f"check_in_time value: {record.check_in_time}")
|
||||||
|
print(f"verification_photo exists: {record.verification_photo is not None}")
|
||||||
|
print(f"verification_status: {record.verification_status}")
|
||||||
|
print(f"==================================")
|
||||||
|
|
||||||
|
# Check if user has permission to view
|
||||||
|
# Allow admin and payroll staff to view verification details
|
||||||
|
if session.get('role') not in ['admin', 'payroll']:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Unauthorized access'
|
||||||
|
}), 403
|
||||||
|
|
||||||
|
# Log the access for security audit
|
||||||
|
logger_handler.logger.info(f"User {session.get('username')} ({session.get('role')}) accessed verification details for record {record_id}")
|
||||||
|
|
||||||
|
# Safely format dates/times with error handling
|
||||||
|
try:
|
||||||
|
check_in_date_str = record.check_in_date.strftime('%Y-%m-%d') if record.check_in_date else 'N/A'
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error formatting check_in_date: {e}")
|
||||||
|
check_in_date_str = str(record.check_in_date) if record.check_in_date else 'N/A'
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_in_time_str = record.check_in_time.strftime('%I:%M %p') if record.check_in_time else 'N/A'
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error formatting check_in_time: {e}")
|
||||||
|
check_in_time_str = str(record.check_in_time) if record.check_in_time else 'N/A'
|
||||||
|
|
||||||
|
# Prepare record data with safe formatting
|
||||||
|
try:
|
||||||
|
check_in_date_str = record.check_in_date.strftime('%Y-%m-%d') if record.check_in_date else 'N/A'
|
||||||
|
except:
|
||||||
|
check_in_date_str = str(record.check_in_date) if record.check_in_date else 'N/A'
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_in_time_str = record.check_in_time.strftime('%I:%M %p') if record.check_in_time else 'N/A'
|
||||||
|
except:
|
||||||
|
check_in_time_str = str(record.check_in_time) if record.check_in_time else 'N/A'
|
||||||
|
|
||||||
|
record_data = {
|
||||||
|
'id': record.id,
|
||||||
|
'employee_id': record.employee_id,
|
||||||
|
'location_name': record.location_name or 'Unknown',
|
||||||
|
'check_in_date': check_in_date_str,
|
||||||
|
'check_in_time': check_in_time_str,
|
||||||
|
'location_accuracy': float(record.location_accuracy) if record.location_accuracy else None,
|
||||||
|
'checked_in_address': record.address or 'No address',
|
||||||
|
'verification_photo': record.verification_photo,
|
||||||
|
'verification_status': record.verification_status,
|
||||||
|
'verification_required': record.verification_required,
|
||||||
|
'device_info': record.device_info or 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'record': record_data
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(f"Error getting verification details for record {record_id}: {e}")
|
||||||
|
print(f"❌ Error in get_verification_details for record {record_id}: {e}")
|
||||||
|
import traceback
|
||||||
|
print(f"❌ Traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Error loading verification details'
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/attendance/stats')
|
@app.route('/api/attendance/stats')
|
||||||
@admin_required
|
@admin_required
|
||||||
@@ -9587,4 +9691,4 @@ if __name__ == '__main__':
|
|||||||
app.run(debug=os.environ.get('DEBUG'),
|
app.run(debug=os.environ.get('DEBUG'),
|
||||||
host=os.environ.get('FLASK_HOST'),
|
host=os.environ.get('FLASK_HOST'),
|
||||||
port=os.environ.get('FLASK_PORT'),
|
port=os.environ.get('FLASK_PORT'),
|
||||||
threaded=os.environ.get ('THREADED'))
|
threaded=os.environ.get ('THREADED'))
|
||||||
+199
-1
@@ -686,7 +686,7 @@ tr.modified-record {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
z-index: var(--z-modal);
|
z-index: 99999 !important;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +701,7 @@ tr.modified-record {
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
|
z-index: 100000 !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1365,3 +1366,200 @@ tr.modified-record {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
VERIFICATION REVIEW STYLES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Review Needed Badge */
|
||||||
|
.badge-review-needed {
|
||||||
|
background: #fef3c7 !important;
|
||||||
|
color: #92400e !important;
|
||||||
|
border: 1px solid rgba(146, 64, 14, 0.3) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-review-needed:hover {
|
||||||
|
background: #fde68a !important;
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 8px rgba(146, 64, 14, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verified Badge */
|
||||||
|
.badge-verified {
|
||||||
|
background: #d1fae5 !important;
|
||||||
|
color: #065f46 !important;
|
||||||
|
border: 1px solid rgba(5, 150, 105, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rejected Badge */
|
||||||
|
.badge-rejected {
|
||||||
|
background: #fee2e2 !important;
|
||||||
|
color: #991b1b !important;
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Review Button in Actions Column */
|
||||||
|
.btn-review {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid rgba(146, 64, 14, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-review:hover {
|
||||||
|
background: #92400e;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #92400e;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(146, 64, 14, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verification Photo Modal Specific Styles */
|
||||||
|
#verificationPhotoModal {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-modal-content {
|
||||||
|
max-width: 900px;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-photo-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-photo-large {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-detail-card {
|
||||||
|
background: var(--gray-50, #f9fafb);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--gray-200, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-detail-card h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--gray-600, #6b7280);
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-detail-card p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--gray-900, #0f172a);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--gray-200, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-status-pending {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.verification-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--gray-600, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-loading i {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design for Verification Modal */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.verification-modal-content {
|
||||||
|
width: 98%;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-photo-large {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-details-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve,
|
||||||
|
.btn-reject {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
+318
-89
@@ -37,89 +37,6 @@ function initializeReport() {
|
|||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTableData() {
|
|
||||||
const table = document.getElementById("attendanceTable");
|
|
||||||
if (table) {
|
|
||||||
const rows = table.querySelectorAll("tbody tr");
|
|
||||||
attendanceData = Array.from(rows).map((row, index) => {
|
|
||||||
const cells = row.querySelectorAll("td");
|
|
||||||
|
|
||||||
// Debug: Log the actual cell content
|
|
||||||
if (index < 3) {
|
|
||||||
// Only log first 3 rows for debugging
|
|
||||||
console.log(`=== DEBUGGING ROW ${index + 1} ===`);
|
|
||||||
console.log(
|
|
||||||
`Cell 7 (check-in address):`,
|
|
||||||
cells[7] ? cells[7].innerHTML : "NOT FOUND"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`Cell 8 (accuracy):`,
|
|
||||||
cells[8] ? cells[8].innerHTML : "NOT FOUND"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cells[8]) {
|
|
||||||
const accuracyText = cells[8].textContent;
|
|
||||||
console.log(`Accuracy text:`, accuracyText);
|
|
||||||
|
|
||||||
const milesMatch = accuracyText.match(/(\d+\.?\d*)\s*mi/);
|
|
||||||
const metersMatch = accuracyText.match(/(\d+\.?\d*)\s*m/);
|
|
||||||
console.log(`Miles match:`, milesMatch);
|
|
||||||
console.log(`Meters match:`, metersMatch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: row.dataset.recordId,
|
|
||||||
index: index + 1,
|
|
||||||
employeeId: cells[1] ? cells[1].textContent.trim() : "",
|
|
||||||
employeeName: cells[2] ? cells[2].textContent.trim() : "",
|
|
||||||
location: cells[3] ? cells[3].textContent.trim() : "",
|
|
||||||
event: cells[4] ? cells[4].textContent.trim() : "",
|
|
||||||
date: cells[5] ? cells[5].textContent.trim() : "",
|
|
||||||
time: cells[6] ? cells[6].textContent.trim() : "",
|
|
||||||
qr_address: cells[7]
|
|
||||||
? cells[7].getAttribute("title") || cells[7].textContent.trim()
|
|
||||||
: "",
|
|
||||||
checked_in_address: cells[8]
|
|
||||||
? cells[8].getAttribute("title") || cells[8].textContent.trim()
|
|
||||||
: "",
|
|
||||||
location_accuracy: cells[9] ? extractLocationAccuracy(cells[9]) : null,
|
|
||||||
accuracy_level: cells[9]
|
|
||||||
? extractLocationAccuracyLevel(cells[9])
|
|
||||||
: "unknown",
|
|
||||||
device: cells[10]
|
|
||||||
? cells[10].textContent.trim()
|
|
||||||
: ""
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
filteredData = [...attendanceData];
|
|
||||||
console.log(`Loaded ${attendanceData.length} attendance records`);
|
|
||||||
|
|
||||||
// Debug log for location accuracy data
|
|
||||||
const recordsWithAccuracy = attendanceData.filter(
|
|
||||||
(r) => r.location_accuracy !== null
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`Records with location accuracy: ${recordsWithAccuracy.length}`
|
|
||||||
);
|
|
||||||
if (recordsWithAccuracy.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`Sample records with accuracy:`,
|
|
||||||
recordsWithAccuracy.slice(0, 3).map((r) => ({
|
|
||||||
employeeId: r.employeeId,
|
|
||||||
location_accuracy: r.location_accuracy,
|
|
||||||
accuracy_level: r.accuracy_level,
|
|
||||||
qr_address: r.qr_address,
|
|
||||||
checked_in_address: r.checked_in_address,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log all first 3 records for debugging
|
|
||||||
console.log("First 3 attendance records:", attendanceData.slice(0, 3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractAccuracyValue(cell) {
|
function extractAccuracyValue(cell) {
|
||||||
const text = cell.textContent;
|
const text = cell.textContent;
|
||||||
@@ -563,6 +480,10 @@ function loadTableData() {
|
|||||||
const rows = table.querySelectorAll("tbody tr");
|
const rows = table.querySelectorAll("tbody tr");
|
||||||
attendanceData = Array.from(rows).map((row, index) => {
|
attendanceData = Array.from(rows).map((row, index) => {
|
||||||
const cells = row.querySelectorAll("td");
|
const cells = row.querySelectorAll("td");
|
||||||
|
|
||||||
|
// Extract verification data from the accuracy badge
|
||||||
|
const verificationData = extractVerificationData(cells[9]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.dataset.recordId,
|
id: row.dataset.recordId,
|
||||||
index: index + 1,
|
index: index + 1,
|
||||||
@@ -587,6 +508,8 @@ function loadTableData() {
|
|||||||
? cells[10].textContent.trim()
|
? cells[10].textContent.trim()
|
||||||
: "",
|
: "",
|
||||||
isModified: row.classList.contains('modified-record'),
|
isModified: row.classList.contains('modified-record'),
|
||||||
|
verification_required: verificationData.required,
|
||||||
|
verification_status: verificationData.status
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -661,6 +584,37 @@ function extractLocationAccuracyLevel(cell) {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractVerificationData(cell) {
|
||||||
|
// Extract verification status from badge classes in the HTML
|
||||||
|
if (!cell) {
|
||||||
|
console.log('extractVerificationData: No cell provided');
|
||||||
|
return { required: false, status: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const badge = cell.querySelector('.location-accuracy-badge');
|
||||||
|
if (!badge) {
|
||||||
|
console.log('extractVerificationData: No badge found in cell');
|
||||||
|
return { required: false, status: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('extractVerificationData: Badge classes:', badge.className);
|
||||||
|
|
||||||
|
// Check badge classes for verification status
|
||||||
|
if (badge.classList.contains('badge-review-needed')) {
|
||||||
|
console.log('extractVerificationData: Found pending verification');
|
||||||
|
return { required: true, status: 'pending' };
|
||||||
|
} else if (badge.classList.contains('badge-verified')) {
|
||||||
|
console.log('extractVerificationData: Found approved verification');
|
||||||
|
return { required: true, status: 'approved' };
|
||||||
|
} else if (badge.classList.contains('badge-rejected')) {
|
||||||
|
console.log('extractVerificationData: Found rejected verification');
|
||||||
|
return { required: true, status: 'rejected' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('extractVerificationData: No verification status found, standard badge');
|
||||||
|
return { required: false, status: null };
|
||||||
|
}
|
||||||
|
|
||||||
function createTableRow(record, displayIndex) {
|
function createTableRow(record, displayIndex) {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
row.dataset.recordId = record.id;
|
row.dataset.recordId = record.id;
|
||||||
@@ -675,14 +629,44 @@ function createTableRow(record, displayIndex) {
|
|||||||
console.log(`=== CREATING ROW ${displayIndex} ===`);
|
console.log(`=== CREATING ROW ${displayIndex} ===`);
|
||||||
console.log(`Employee: ${record.employeeId}`);
|
console.log(`Employee: ${record.employeeId}`);
|
||||||
console.log(`Location accuracy: ${record.location_accuracy}`);
|
console.log(`Location accuracy: ${record.location_accuracy}`);
|
||||||
|
console.log(`Verification required: ${record.verification_required}`);
|
||||||
|
console.log(`Verification status: ${record.verification_status}`);
|
||||||
console.log(`QR address: ${record.qr_address}`);
|
console.log(`QR address: ${record.qr_address}`);
|
||||||
console.log(`Check-in address: ${record.checked_in_address}`);
|
console.log(`Check-in address: ${record.checked_in_address}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create location accuracy badge HTML
|
// Create location accuracy badge HTML - check verification status first
|
||||||
const locationAccuracyBadge =
|
let locationAccuracyBadge;
|
||||||
record.location_accuracy !== null
|
|
||||||
? `<span class="location-accuracy-badge accuracy-${
|
if (record.verification_required && record.verification_status === 'pending') {
|
||||||
|
// Show Review Needed badge for pending verification
|
||||||
|
locationAccuracyBadge = `<span class="location-accuracy-badge badge-review-needed"
|
||||||
|
onclick="openVerificationPhotoModal('${record.id}')"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
title="Click to review verification photo - Distance: ${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} miles">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Review Needed
|
||||||
|
<small>(${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi)</small>
|
||||||
|
</span>`;
|
||||||
|
} else if (record.verification_status === 'approved') {
|
||||||
|
// Show Verified badge for approved verification
|
||||||
|
locationAccuracyBadge = `<span class="location-accuracy-badge badge-verified"
|
||||||
|
title="Verification approved - Distance: ${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} miles">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
Verified
|
||||||
|
<small>(${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi)</small>
|
||||||
|
</span>`;
|
||||||
|
} else if (record.verification_status === 'rejected') {
|
||||||
|
// Show Rejected badge for rejected verification
|
||||||
|
locationAccuracyBadge = `<span class="location-accuracy-badge badge-rejected"
|
||||||
|
title="Verification rejected - Distance: ${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} miles">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
Rejected
|
||||||
|
<small>(${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi)</small>
|
||||||
|
</span>`;
|
||||||
|
} else if (record.location_accuracy !== null) {
|
||||||
|
// Show standard location accuracy badge
|
||||||
|
locationAccuracyBadge = `<span class="location-accuracy-badge accuracy-${
|
||||||
record.accuracy_level
|
record.accuracy_level
|
||||||
}"
|
}"
|
||||||
title="Distance between QR location and check-in location: ${
|
title="Distance between QR location and check-in location: ${
|
||||||
@@ -691,11 +675,14 @@ function createTableRow(record, displayIndex) {
|
|||||||
<i class="fas fa-ruler"></i>
|
<i class="fas fa-ruler"></i>
|
||||||
${record.location_accuracy.toFixed(3)} mi
|
${record.location_accuracy.toFixed(3)} mi
|
||||||
<small>(${record.accuracy_level})</small>
|
<small>(${record.accuracy_level})</small>
|
||||||
</span>`
|
</span>`;
|
||||||
: `<span class="location-accuracy-badge accuracy-unknown" title="Location accuracy could not be calculated">
|
} else {
|
||||||
|
// No accuracy data
|
||||||
|
locationAccuracyBadge = `<span class="location-accuracy-badge accuracy-unknown" title="Location accuracy could not be calculated">
|
||||||
<i class="fas fa-question-circle"></i>
|
<i class="fas fa-question-circle"></i>
|
||||||
Unknown
|
Unknown
|
||||||
</span>`;
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Address display logic based on location accuracy
|
// Address display logic based on location accuracy
|
||||||
let addressDisplayHTML = "";
|
let addressDisplayHTML = "";
|
||||||
@@ -849,6 +836,15 @@ function createTableRow(record, displayIndex) {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="record-actions">
|
<div class="record-actions">
|
||||||
|
${
|
||||||
|
record.verification_required && record.verification_status === 'pending'
|
||||||
|
? `<button onclick="openVerificationPhotoModal('${record.id}')"
|
||||||
|
class="action-btn btn-review"
|
||||||
|
title="Review Verification Photo">
|
||||||
|
<i class="fas fa-camera"></i>
|
||||||
|
</button>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
${
|
${
|
||||||
hasEditPermission
|
hasEditPermission
|
||||||
? `
|
? `
|
||||||
@@ -1181,3 +1177,236 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
console.log("Enhanced export functionality initialized");
|
console.log("Enhanced export functionality initialized");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VERIFICATION PHOTO REVIEW FUNCTIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open verification photo modal for review
|
||||||
|
* @param {string} recordId - The attendance record ID
|
||||||
|
*/
|
||||||
|
function openVerificationPhotoModal(recordId) {
|
||||||
|
console.log(`Opening verification photo modal for record: ${recordId}`);
|
||||||
|
|
||||||
|
const modal = document.getElementById("verificationPhotoModal");
|
||||||
|
const modalBody = document.getElementById("verificationPhotoModalBody");
|
||||||
|
|
||||||
|
if (!modal || !modalBody) {
|
||||||
|
console.error("Verification photo modal elements not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="verification-loading">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<p>Loading verification photo...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.style.display = "block";
|
||||||
|
|
||||||
|
// Fetch record details with verification photo
|
||||||
|
fetch(`/api/attendance/${recordId}/verification-details`)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch verification details");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message || "Failed to load verification data");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVerificationPhotoModal(data.record);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error loading verification photo:", error);
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="verification-error">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<p>Error loading verification photo. Please try again.</p>
|
||||||
|
<button onclick="closeVerificationPhotoModal()" class="btn btn-secondary">Close</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render verification photo modal content
|
||||||
|
* @param {Object} record - The attendance record with verification data
|
||||||
|
*/
|
||||||
|
function renderVerificationPhotoModal(record) {
|
||||||
|
const modalBody = document.getElementById("verificationPhotoModalBody");
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div class="verification-photo-review">
|
||||||
|
<!-- Employee & Date Info -->
|
||||||
|
<div class="verification-header-info">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
Employee: ${record.employee_id}
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<i class="fas fa-calendar"></i>
|
||||||
|
${record.check_in_date} at ${record.check_in_time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verification Photo -->
|
||||||
|
<div class="verification-photo-container">
|
||||||
|
${
|
||||||
|
record.verification_photo
|
||||||
|
? `<img src="${record.verification_photo}"
|
||||||
|
alt="Verification Photo"
|
||||||
|
class="verification-photo-large"
|
||||||
|
onclick="window.open(this.src, '_blank')"
|
||||||
|
style="cursor: zoom-in;"
|
||||||
|
title="Click to view full size" />`
|
||||||
|
: `<div class="no-photo">
|
||||||
|
<i class="fas fa-image"></i>
|
||||||
|
<p>No verification photo available</p>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Details -->
|
||||||
|
<div class="verification-details-grid">
|
||||||
|
<div class="verification-detail-card">
|
||||||
|
<h4>Location Name</h4>
|
||||||
|
<p>${record.location_name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-detail-card">
|
||||||
|
<h4>Distance from QR</h4>
|
||||||
|
<p>${parseFloat(record.location_accuracy).toFixed(3)} miles</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-detail-card">
|
||||||
|
<h4>Verification Status</h4>
|
||||||
|
<p>
|
||||||
|
<span class="verification-status-pending">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
Pending Review
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-detail-card">
|
||||||
|
<h4>Device</h4>
|
||||||
|
<p>${record.device_info || "Unknown"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Address Information -->
|
||||||
|
<div class="verification-detail-card" style="grid-column: 1 / -1;">
|
||||||
|
<h4>Check-in Address</h4>
|
||||||
|
<p>${record.checked_in_address || "No address recorded"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="verification-actions">
|
||||||
|
<button onclick="updateVerificationStatus(${
|
||||||
|
record.id
|
||||||
|
}, 'approved')" class="btn-approve">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
Approve Check-in
|
||||||
|
</button>
|
||||||
|
<button onclick="updateVerificationStatus(${
|
||||||
|
record.id
|
||||||
|
}, 'rejected')" class="btn-reject">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
Reject Check-in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modalBody.innerHTML = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close verification photo modal
|
||||||
|
*/
|
||||||
|
function closeVerificationPhotoModal() {
|
||||||
|
const modal = document.getElementById("verificationPhotoModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update verification status (approve/reject)
|
||||||
|
* @param {number} recordId - The attendance record ID
|
||||||
|
* @param {string} status - The new status ('approved' or 'rejected')
|
||||||
|
*/
|
||||||
|
function updateVerificationStatus(recordId, status) {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to ${status} this verification?\n\nThis action will be logged for audit purposes.`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Updating verification status for record ${recordId} to ${status}`);
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const modalBody = document.getElementById("verificationPhotoModalBody");
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="verification-loading">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<p>Updating verification status...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Send update request
|
||||||
|
fetch(`/verification-review/${recordId}/update`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: status,
|
||||||
|
note: `Verification ${status} from attendance report review`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(
|
||||||
|
`Verification ${status} successfully!\n\nThe page will reload to show the updated status.`
|
||||||
|
);
|
||||||
|
closeVerificationPhotoModal();
|
||||||
|
// Reload the page to show updated status
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Failed to update verification status");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error updating verification status:", error);
|
||||||
|
alert(`Error: ${error.message}\n\nPlease try again.`);
|
||||||
|
// Reload modal to show previous state
|
||||||
|
openVerificationPhotoModal(recordId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close verification modal when clicking outside
|
||||||
|
window.addEventListener("click", function (event) {
|
||||||
|
const verificationModal = document.getElementById("verificationPhotoModal");
|
||||||
|
|
||||||
|
if (event.target === verificationModal) {
|
||||||
|
closeVerificationPhotoModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcut for closing verification modal
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeVerificationPhotoModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -266,11 +266,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if has_location_accuracy_feature and record.location_accuracy %}
|
{% if record.verification_required and record.verification_status == 'pending' %}
|
||||||
<!-- Show location accuracy in miles -->
|
<!-- Show Review Needed badge for pending verification -->
|
||||||
|
<div class="location-accuracy-info">
|
||||||
|
<span class="location-accuracy-badge badge-review-needed"
|
||||||
|
onclick="openVerificationPhotoModal('{{ record.id }}')"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
title="Click to review verification photo - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Review Needed
|
||||||
|
<small>({{ '%.3f'|format(record.location_accuracy) }} mi)</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% elif record.verification_status == 'approved' %}
|
||||||
|
<!-- Show Verified badge for approved verification -->
|
||||||
|
<div class="location-accuracy-info">
|
||||||
|
<span class="location-accuracy-badge badge-verified"
|
||||||
|
title="Verification approved - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
Verified
|
||||||
|
<small>({{ '%.3f'|format(record.location_accuracy) }} mi)</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% elif record.verification_status == 'rejected' %}
|
||||||
|
<!-- Show Rejected badge for rejected verification -->
|
||||||
|
<div class="location-accuracy-info">
|
||||||
|
<span class="location-accuracy-badge badge-rejected"
|
||||||
|
title="Verification rejected - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
|
||||||
|
<i class="fas fa-times-circle"></i>
|
||||||
|
Rejected
|
||||||
|
<small>({{ '%.3f'|format(record.location_accuracy) }} mi)</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% elif has_location_accuracy_feature and record.location_accuracy %}
|
||||||
|
<!-- Show location accuracy in miles (normal case) -->
|
||||||
<div class="location-accuracy-info">
|
<div class="location-accuracy-info">
|
||||||
<span class="location-accuracy-badge accuracy-{{ record.accuracy_level }}"
|
<span class="location-accuracy-badge accuracy-{{ record.accuracy_level }}"
|
||||||
title="Distance between QR location and check-in location: {{ record.location_accuracy }} miles">
|
title="Distance between QR location and check-in location: {{ record.location_accuracy }} miles">
|
||||||
<i class="fas fa-ruler"></i>
|
<i class="fas fa-ruler"></i>
|
||||||
{{ "%.3f"|format(record.location_accuracy) }} mi
|
{{ "%.3f"|format(record.location_accuracy) }} mi
|
||||||
<small>({{ record.accuracy_level }})</small>
|
<small>({{ record.accuracy_level }})</small>
|
||||||
@@ -280,7 +312,7 @@
|
|||||||
<!-- Fallback to GPS accuracy in meters -->
|
<!-- Fallback to GPS accuracy in meters -->
|
||||||
<div class="accuracy-info">
|
<div class="accuracy-info">
|
||||||
<span class="accuracy-badge accuracy-{{ record.accuracy_level }}"
|
<span class="accuracy-badge accuracy-{{ record.accuracy_level }}"
|
||||||
title="GPS accuracy: {{ record.gps_accuracy }}m">
|
title="GPS accuracy: {{ record.gps_accuracy }}m">
|
||||||
<i class="fas fa-crosshairs"></i>
|
<i class="fas fa-crosshairs"></i>
|
||||||
{{ "%.1f"|format(record.gps_accuracy) }}m
|
{{ "%.1f"|format(record.gps_accuracy) }}m
|
||||||
<small>(gps)</small>
|
<small>(gps)</small>
|
||||||
@@ -306,6 +338,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="record-actions">
|
<div class="record-actions">
|
||||||
|
{% if record.verification_required and record.verification_status == 'pending' %}
|
||||||
|
<!-- Show Review button for pending verification -->
|
||||||
|
<button onclick="openVerificationPhotoModal('{{ record.id }}')"
|
||||||
|
class="action-btn btn-review"
|
||||||
|
title="Review Verification Photo">
|
||||||
|
<i class="fas fa-camera"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if session.role in ['admin'] %}
|
{% if session.role in ['admin'] %}
|
||||||
<button onclick="editRecord('{{ record.id }}')"
|
<button onclick="editRecord('{{ record.id }}')"
|
||||||
class="action-btn btn-edit"
|
class="action-btn btn-edit"
|
||||||
@@ -399,6 +440,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Verification Photo Review Modal -->
|
||||||
|
<div id="verificationPhotoModal" class="modal">
|
||||||
|
<div class="modal-content verification-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-camera-retro"></i>
|
||||||
|
Verification Photo Review
|
||||||
|
</h2>
|
||||||
|
<button class="close-btn" onclick="closeVerificationPhotoModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="verificationPhotoModalBody">
|
||||||
|
<!-- Content will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
|
|||||||
Reference in New Issue
Block a user