From 03c75d5d303e0e506827773ba7329c3860f105f3 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Wed, 13 Aug 2025 22:40:57 -0400 Subject: [PATCH] Update attendance report functions: edit & delete record for admin & payroll users --- app.py | 144 +++++++++++++++++++++ static/css/attendance.css | 68 ++++++++++ static/js/attendance_report.js | 97 +++++++++++++- templates/attendance_report.html | 150 ++++++++++++++++----- templates/edit_attendance.html | 215 +++++++++++++++++++++++++++++++ 5 files changed, 637 insertions(+), 37 deletions(-) create mode 100644 templates/edit_attendance.html diff --git a/app.py b/app.py index e8b84df..7aacc0c 100644 --- a/app.py +++ b/app.py @@ -3240,6 +3240,150 @@ def attendance_report(): flash('Error loading attendance report. Please check the server logs for details.', 'error') return redirect(url_for('dashboard')) +@app.route('/attendance//edit', methods=['GET', 'POST']) +@login_required +@log_database_operations('attendance_update') +def edit_attendance(record_id): + """Edit attendance record (Admin and Payroll only)""" + # Check if user has permission to edit attendance records + if session.get('role') not in ['admin', 'payroll']: + flash('Access denied. Only administrators and payroll staff can edit attendance records.', 'error') + return redirect(url_for('attendance_report')) + + try: + attendance_record = AttendanceData.query.get_or_404(record_id) + + if request.method == 'POST': + # Track changes for logging + changes = {} + old_values = { + 'employee_id': attendance_record.employee_id, + 'check_in_date': attendance_record.check_in_date, + 'check_in_time': attendance_record.check_in_time, + 'location_name': attendance_record.location_name + } + + # Update attendance record fields + new_employee_id = request.form['employee_id'].strip().upper() + new_check_in_date = datetime.strptime(request.form['check_in_date'], '%Y-%m-%d').date() + new_check_in_time = datetime.strptime(request.form['check_in_time'], '%H:%M').time() + new_location_name = request.form['location_name'].strip() + + # Track what changed + if attendance_record.employee_id != new_employee_id: + changes['employee_id'] = f"{attendance_record.employee_id} → {new_employee_id}" + if attendance_record.check_in_date != new_check_in_date: + changes['check_in_date'] = f"{attendance_record.check_in_date} → {new_check_in_date}" + if attendance_record.check_in_time != new_check_in_time: + changes['check_in_time'] = f"{attendance_record.check_in_time} → {new_check_in_time}" + if attendance_record.location_name != new_location_name: + changes['location_name'] = f"{attendance_record.location_name} → {new_location_name}" + + # Apply changes + attendance_record.employee_id = new_employee_id + attendance_record.check_in_date = new_check_in_date + attendance_record.check_in_time = new_check_in_time + attendance_record.location_name = new_location_name + attendance_record.updated_timestamp = datetime.utcnow() + + db.session.commit() + + # Log the successful update + if changes: + logger_handler.log_security_event( + event_type="attendance_record_update", + description=f"{session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}", + severity="MEDIUM", + additional_data={'record_id': record_id, 'changes': changes, 'user_role': session.get('role')} + ) + print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}: {changes}") + + flash(f'Attendance record for {new_employee_id} updated successfully!', 'success') + return redirect(url_for('attendance_report')) + + # GET request - show edit form + # Get available QR codes for location dropdown + qr_codes = QRCode.query.filter_by(active_status=True).all() + + return render_template('edit_attendance.html', + attendance_record=attendance_record, + qr_codes=qr_codes) + + except Exception as e: + db.session.rollback() + logger_handler.log_database_error('attendance_update', e) + print(f"[LOG] Error updating attendance record {record_id}: {e}") + flash('Error updating attendance record. Please try again.', 'error') + return redirect(url_for('attendance_report')) + +@app.route('/attendance//delete', methods=['POST']) +@login_required +@log_database_operations('attendance_delete') +def delete_attendance(record_id): + """Delete attendance record (Admin and Payroll only)""" + # Check if user has permission to delete attendance records + if session.get('role') not in ['admin', 'payroll']: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({ + 'success': False, + 'message': 'Access denied. Only administrators and payroll staff can delete attendance records.' + }), 403 + else: + flash('Access denied. Only administrators and payroll staff can delete attendance records.', 'error') + return redirect(url_for('attendance_report')) + + try: + attendance_record = AttendanceData.query.get_or_404(record_id) + + # Store record info for logging before deletion + employee_id = attendance_record.employee_id + location_name = attendance_record.location_name + check_in_date = attendance_record.check_in_date + + # Log the deletion + logger_handler.log_security_event( + event_type="attendance_record_deletion", + description=f"{session.get('role', 'unknown').title()} {session.get('username')} deleted attendance record {record_id}", + severity="HIGH", + additional_data={ + 'record_id': record_id, + 'employee_id': employee_id, + 'location_name': location_name, + 'check_in_date': str(check_in_date), + 'user_role': session.get('role') + } + ) + + # Delete the record + db.session.delete(attendance_record) + db.session.commit() + + print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} deleted attendance record {record_id} for employee {employee_id}") + + # Return JSON response for AJAX requests + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({ + 'success': True, + 'message': f'Attendance record for {employee_id} deleted successfully!' + }) + else: + flash(f'Attendance record for {employee_id} deleted successfully!', 'success') + return redirect(url_for('attendance_report')) + + except Exception as e: + db.session.rollback() + logger_handler.log_database_error('attendance_delete', e) + print(f"[LOG] Error deleting attendance record {record_id}: {e}") + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({ + 'success': False, + 'message': 'Error deleting attendance record. Please try again.' + }), 500 + else: + flash('Error deleting attendance record. Please try again.', 'error') + return redirect(url_for('attendance_report')) + @app.route('/api/attendance/stats') @admin_required def attendance_stats_api(): diff --git a/static/css/attendance.css b/static/css/attendance.css index 9aaeaa7..eba85ac 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -1284,3 +1284,71 @@ display: none; } } +/* Delete button styling */ +.btn-delete { + background: var(--danger-light); + color: var(--danger-color); + border: 1px solid rgba(220, 38, 38, 0.2); +} + +.btn-delete:hover { + background: var(--danger-color); + color: var(--white); + border-color: var(--danger-color); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +/* Enhanced action buttons container */ +.record-actions { + display: flex; + gap: var(--spacing-2); + justify-content: center; +} + +/* Action button improvements */ +.action-btn { + width: 36px; + height: 36px; + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: all var(--transition); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-sm); + border: 1px solid transparent; +} + +.action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.action-btn:active { + transform: translateY(0); +} + +/* Form styling for edit page */ +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } + + .record-actions { + flex-direction: column; + gap: var(--spacing-1); + } + + .action-btn { + width: 100%; + height: 32px; + } +} diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 4ac9fd7..201cb45 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -379,11 +379,26 @@ function createTableRow(record, displayIndex) {
+ ${ + hasEditPermission + ? ` + + ` + : ` + + + + ` + }
`; @@ -511,9 +526,72 @@ function updateFilterStats() { // Enhanced record actions function editRecord(recordId) { + // Check permissions before allowing edit + if (!hasEditPermission) { + alert( + "Access denied. Only administrators and payroll staff can edit attendance records." + ); + return; + } + console.log(`Edit record: ${recordId}`); - // Implement edit functionality - alert("Edit functionality to be implemented"); + // Log the action + console.log(`[LOG] User attempting to edit attendance record: ${recordId}`); + + // Redirect to edit page + window.location.href = `/attendance/${recordId}/edit`; +} + +function deleteRecord(recordId, employeeId) { + // Check permissions before allowing delete + if (!hasEditPermission) { + alert( + "Access denied. Only administrators and payroll staff can delete attendance records." + ); + return; + } + + console.log(`Delete record: ${recordId}`); + + // Confirmation dialog + const confirmMessage = `Are you sure you want to delete the attendance record for employee "${employeeId}"?\n\nThis action cannot be undone.`; + + if (confirm(confirmMessage)) { + console.log( + `[LOG] User confirmed deletion of attendance record: ${recordId}` + ); + + // Send delete request + fetch(`/attendance/${recordId}/delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + console.log( + `[LOG] Successfully deleted attendance record: ${recordId}` + ); + alert("Attendance record deleted successfully!"); + window.location.reload(); + } else { + console.error( + `[LOG] Failed to delete attendance record: ${recordId} - ${data.message}` + ); + alert(data.message || "Error deleting record. Please try again."); + } + }) + .catch((error) => { + console.error( + `[LOG] Error during attendance record deletion: ${recordId}`, + error + ); + alert("Error deleting record. Please try again."); + }); + } } function closeModal() { @@ -696,11 +774,26 @@ function createTableRow(record, displayIndex) {
+ ${ + hasEditPermission + ? ` + + ` + : ` + + + + ` + }
`; diff --git a/templates/attendance_report.html b/templates/attendance_report.html index fb981fd..999bde9 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -266,11 +266,22 @@
+ {% if session.role in ['admin', 'payroll'] %} + + {% else %} + + + + {% endif %}
@@ -353,6 +364,14 @@ {% block extra_scripts %} {% endblock %} \ No newline at end of file diff --git a/templates/edit_attendance.html b/templates/edit_attendance.html new file mode 100644 index 0000000..d484c5b --- /dev/null +++ b/templates/edit_attendance.html @@ -0,0 +1,215 @@ +{% extends "base_authenticated.html" %} {% block title %}Edit Attendance Record +- QR Code Management{% endblock %} {% block extra_head %} + + +{% endblock %} {% block content %} +
+
+

+ + Edit Attendance Record +

+

Modify attendance record details (Admin & Payroll Access)

+
+ + +
+

Current Record Information

+

Record ID: {{ attendance_record.id }}

+

+ Current Employee: {{ attendance_record.employee_id }} +

+

+ Current Date: {{ + attendance_record.check_in_date.strftime('%Y-%m-%d') }} +

+

+ Current Time: {{ + attendance_record.check_in_time.strftime('%H:%M') }} +

+

+ Current Location: {{ attendance_record.location_name }} +

+
+ +
+ +
+

Employee Information

+
+ + +
+
+ + +
+

Check-in Details

+
+
+ + +
+
+ + +
+
+
+ + +
+

Location Information

+
+ + +
+
+ + +
+ + + + Cancel + +
+
+
+{% endblock %} {% block extra_scripts %} + +{% endblock %}