code changes

This commit is contained in:
2025-12-18 16:07:35 -05:00
parent d234e366bd
commit dbdbdffb06
4 changed files with 686 additions and 98 deletions
+107 -3
View File
@@ -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}")
@@ -5407,6 +5426,91 @@ def update_verification_status(record_id):
'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
def attendance_stats_api(): def attendance_stats_api():
+199 -1
View File
@@ -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
View File
@@ -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();
}
});
+61 -4
View File
@@ -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()">&times;</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 %}