code changes
This commit is contained in:
@@ -4774,7 +4774,10 @@ def attendance_report():
|
||||
ad.device_info,
|
||||
ad.created_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
|
||||
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
|
||||
@@ -4799,7 +4802,10 @@ def attendance_report():
|
||||
ad.device_info,
|
||||
ad.created_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
|
||||
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
|
||||
@@ -4885,8 +4891,21 @@ def attendance_report():
|
||||
'device_info': record[12],
|
||||
'created_timestamp': record[13],
|
||||
'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)
|
||||
except Exception as rec_error:
|
||||
print(f"⚠️ Error processing record: {rec_error}")
|
||||
@@ -5407,6 +5426,91 @@ def update_verification_status(record_id):
|
||||
'message': 'Error updating verification status'
|
||||
}), 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')
|
||||
@admin_required
|
||||
def attendance_stats_api():
|
||||
|
||||
+199
-1
@@ -686,7 +686,7 @@ tr.modified-record {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: var(--z-modal);
|
||||
z-index: 99999 !important;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
@@ -701,6 +701,7 @@ tr.modified-record {
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
z-index: 100000 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1365,3 +1366,200 @@ tr.modified-record {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
const text = cell.textContent;
|
||||
@@ -563,6 +480,10 @@ function loadTableData() {
|
||||
const rows = table.querySelectorAll("tbody tr");
|
||||
attendanceData = Array.from(rows).map((row, index) => {
|
||||
const cells = row.querySelectorAll("td");
|
||||
|
||||
// Extract verification data from the accuracy badge
|
||||
const verificationData = extractVerificationData(cells[9]);
|
||||
|
||||
return {
|
||||
id: row.dataset.recordId,
|
||||
index: index + 1,
|
||||
@@ -587,6 +508,8 @@ function loadTableData() {
|
||||
? cells[10].textContent.trim()
|
||||
: "",
|
||||
isModified: row.classList.contains('modified-record'),
|
||||
verification_required: verificationData.required,
|
||||
verification_status: verificationData.status
|
||||
};
|
||||
});
|
||||
|
||||
@@ -661,6 +584,37 @@ function extractLocationAccuracyLevel(cell) {
|
||||
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) {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.recordId = record.id;
|
||||
@@ -675,14 +629,44 @@ function createTableRow(record, displayIndex) {
|
||||
console.log(`=== CREATING ROW ${displayIndex} ===`);
|
||||
console.log(`Employee: ${record.employeeId}`);
|
||||
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(`Check-in address: ${record.checked_in_address}`);
|
||||
}
|
||||
|
||||
// Create location accuracy badge HTML
|
||||
const locationAccuracyBadge =
|
||||
record.location_accuracy !== null
|
||||
? `<span class="location-accuracy-badge accuracy-${
|
||||
// Create location accuracy badge HTML - check verification status first
|
||||
let locationAccuracyBadge;
|
||||
|
||||
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
|
||||
}"
|
||||
title="Distance between QR location and check-in location: ${
|
||||
@@ -691,11 +675,14 @@ function createTableRow(record, displayIndex) {
|
||||
<i class="fas fa-ruler"></i>
|
||||
${record.location_accuracy.toFixed(3)} mi
|
||||
<small>(${record.accuracy_level})</small>
|
||||
</span>`
|
||||
: `<span class="location-accuracy-badge accuracy-unknown" title="Location accuracy could not be calculated">
|
||||
</span>`;
|
||||
} 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>
|
||||
Unknown
|
||||
</span>`;
|
||||
}
|
||||
|
||||
// Address display logic based on location accuracy
|
||||
let addressDisplayHTML = "";
|
||||
@@ -849,6 +836,15 @@ function createTableRow(record, displayIndex) {
|
||||
</td>
|
||||
<td>
|
||||
<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
|
||||
? `
|
||||
@@ -1181,3 +1177,236 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
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>
|
||||
</td>
|
||||
<td>
|
||||
{% if has_location_accuracy_feature and record.location_accuracy %}
|
||||
<!-- Show location accuracy in miles -->
|
||||
{% if record.verification_required and record.verification_status == 'pending' %}
|
||||
<!-- 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">
|
||||
<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>
|
||||
{{ "%.3f"|format(record.location_accuracy) }} mi
|
||||
<small>({{ record.accuracy_level }})</small>
|
||||
@@ -280,7 +312,7 @@
|
||||
<!-- Fallback to GPS accuracy in meters -->
|
||||
<div class="accuracy-info">
|
||||
<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>
|
||||
{{ "%.1f"|format(record.gps_accuracy) }}m
|
||||
<small>(gps)</small>
|
||||
@@ -306,6 +338,15 @@
|
||||
</td>
|
||||
<td>
|
||||
<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'] %}
|
||||
<button onclick="editRecord('{{ record.id }}')"
|
||||
class="action-btn btn-edit"
|
||||
@@ -399,6 +440,22 @@
|
||||
</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 %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
|
||||
Reference in New Issue
Block a user