From dbdbdffb06a18dd44e7cbd7d6c89a58ca9585a2b Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 18 Dec 2025 16:07:35 -0500 Subject: [PATCH] code changes --- app.py | 112 ++++++++- static/css/attendance.css | 200 ++++++++++++++- static/js/attendance_report.js | 407 ++++++++++++++++++++++++------- templates/attendance_report.html | 65 ++++- 4 files changed, 686 insertions(+), 98 deletions(-) diff --git a/app.py b/app.py index 73b919f..c308a31 100644 --- a/app.py +++ b/app.py @@ -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}") @@ -5406,6 +5425,91 @@ def update_verification_status(record_id): 'success': False, 'message': 'Error updating verification status' }), 500 + +@app.route('/api/attendance//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 @@ -9587,4 +9691,4 @@ if __name__ == '__main__': app.run(debug=os.environ.get('DEBUG'), host=os.environ.get('FLASK_HOST'), port=os.environ.get('FLASK_PORT'), - threaded=os.environ.get ('THREADED')) + threaded=os.environ.get ('THREADED')) \ No newline at end of file diff --git a/static/css/attendance.css b/static/css/attendance.css index 196bd70..c7bbb2f 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -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; + } +} \ No newline at end of file diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 4073a3d..81f1a76 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -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 - ? ` + + Review Needed + (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) + `; + } else if (record.verification_status === 'approved') { + // Show Verified badge for approved verification + locationAccuracyBadge = ` + + Verified + (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) + `; + } else if (record.verification_status === 'rejected') { + // Show Rejected badge for rejected verification + locationAccuracyBadge = ` + + Rejected + (${record.location_accuracy ? record.location_accuracy.toFixed(3) : 'N/A'} mi) + `; + } else if (record.location_accuracy !== null) { + // Show standard location accuracy badge + locationAccuracyBadge = ` ${record.location_accuracy.toFixed(3)} mi (${record.accuracy_level}) - ` - : ` + `; + } else { + // No accuracy data + locationAccuracyBadge = ` Unknown `; + } // Address display logic based on location accuracy let addressDisplayHTML = ""; @@ -849,6 +836,15 @@ function createTableRow(record, displayIndex) {
+ ${ + record.verification_required && record.verification_status === 'pending' + ? `` + : '' + } ${ 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 = ` +
+ +

Loading verification photo...

+
+ `; + + 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 = ` +
+ +

Error loading verification photo. Please try again.

+ +
+ `; + }); +} + +/** + * Render verification photo modal content + * @param {Object} record - The attendance record with verification data + */ +function renderVerificationPhotoModal(record) { + const modalBody = document.getElementById("verificationPhotoModalBody"); + + const content = ` +
+ +
+

+ + Employee: ${record.employee_id} +

+

+ + ${record.check_in_date} at ${record.check_in_time} +

+
+ + +
+ ${ + record.verification_photo + ? `Verification Photo` + : `
+ +

No verification photo available

+
` + } +
+ + +
+
+

Location Name

+

${record.location_name}

+
+ +
+

Distance from QR

+

${parseFloat(record.location_accuracy).toFixed(3)} miles

+
+ +
+

Verification Status

+

+ + + Pending Review + +

+
+ +
+

Device

+

${record.device_info || "Unknown"}

+
+
+ + +
+

Check-in Address

+

${record.checked_in_address || "No address recorded"}

+
+ + +
+ + +
+
+ `; + + 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 = ` +
+ +

Updating verification status...

+
+ `; + + // 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(); + } +}); \ No newline at end of file diff --git a/templates/attendance_report.html b/templates/attendance_report.html index 5a6c627..6d7da26 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -266,11 +266,43 @@
- {% if has_location_accuracy_feature and record.location_accuracy %} - + {% if record.verification_required and record.verification_status == 'pending' %} + +
+ + + Review Needed + ({{ '%.3f'|format(record.location_accuracy) }} mi) + +
+ {% elif record.verification_status == 'approved' %} + +
+ + + Verified + ({{ '%.3f'|format(record.location_accuracy) }} mi) + +
+ {% elif record.verification_status == 'rejected' %} + +
+ + + Rejected + ({{ '%.3f'|format(record.location_accuracy) }} mi) + +
+ {% elif has_location_accuracy_feature and record.location_accuracy %} +
+ title="Distance between QR location and check-in location: {{ record.location_accuracy }} miles"> {{ "%.3f"|format(record.location_accuracy) }} mi ({{ record.accuracy_level }}) @@ -280,7 +312,7 @@
+ title="GPS accuracy: {{ record.gps_accuracy }}m"> {{ "%.1f"|format(record.gps_accuracy) }}m (gps) @@ -306,6 +338,15 @@
+ {% if record.verification_required and record.verification_status == 'pending' %} + + + {% endif %} + {% if session.role in ['admin'] %}
+ + + {% endblock %} {% block extra_scripts %}