Update audit information for editing attendance record
This commit is contained in:
@@ -4243,6 +4243,14 @@ def edit_attendance(record_id):
|
|||||||
attendance_record = AttendanceData.query.get_or_404(record_id)
|
attendance_record = AttendanceData.query.get_or_404(record_id)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
# Get the audit note from form - REQUIRED
|
||||||
|
edit_note = request.form.get('edit_note', '').strip()
|
||||||
|
if not edit_note:
|
||||||
|
flash('Edit reason is required for audit purposes.', 'error')
|
||||||
|
return render_template('edit_attendance.html',
|
||||||
|
attendance_record=attendance_record,
|
||||||
|
qr_codes=QRCode.query.filter_by(active_status=True).all())
|
||||||
|
|
||||||
# Track changes for logging
|
# Track changes for logging
|
||||||
changes = {}
|
changes = {}
|
||||||
old_values = {
|
old_values = {
|
||||||
@@ -4275,19 +4283,55 @@ def edit_attendance(record_id):
|
|||||||
attendance_record.location_name = new_location_name
|
attendance_record.location_name = new_location_name
|
||||||
attendance_record.updated_timestamp = datetime.utcnow()
|
attendance_record.updated_timestamp = datetime.utcnow()
|
||||||
|
|
||||||
|
# Store the audit note with timestamp and user info
|
||||||
|
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
username = session.get('username', 'Unknown')
|
||||||
|
role = session.get('role', 'unknown')
|
||||||
|
|
||||||
|
new_note_entry = f"[{timestamp}] {role.title()} '{username}': {edit_note}"
|
||||||
|
|
||||||
|
if attendance_record.edit_note:
|
||||||
|
# Append to existing notes
|
||||||
|
attendance_record.edit_note = f"{attendance_record.edit_note}\n\n{new_note_entry}"
|
||||||
|
else:
|
||||||
|
# First edit note
|
||||||
|
attendance_record.edit_note = new_note_entry
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Log the successful update
|
# Enhanced logging with audit note
|
||||||
if changes:
|
if changes:
|
||||||
logger_handler.log_security_event(
|
logger_handler.log_security_event(
|
||||||
event_type="attendance_record_update",
|
event_type="attendance_record_update",
|
||||||
description=f"{session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}",
|
description=f"{session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}",
|
||||||
severity="MEDIUM",
|
severity="MEDIUM",
|
||||||
additional_data={'record_id': record_id, 'changes': changes, 'user_role': session.get('role')}
|
additional_data={
|
||||||
|
'record_id': record_id,
|
||||||
|
'changes': changes,
|
||||||
|
'user_role': session.get('role'),
|
||||||
|
'edit_reason': edit_note,
|
||||||
|
'editor_username': session.get('username')
|
||||||
|
}
|
||||||
)
|
)
|
||||||
print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}: {changes}")
|
print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}: {changes}")
|
||||||
|
print(f"[LOG] Edit reason: {edit_note}")
|
||||||
|
else:
|
||||||
|
# Log even if no changes were made (for audit purposes)
|
||||||
|
logger_handler.log_security_event(
|
||||||
|
event_type="attendance_record_edit_no_changes",
|
||||||
|
description=f"{session.get('role', 'unknown').title()} {session.get('username')} accessed edit form for record {record_id} but made no changes",
|
||||||
|
severity="LOW",
|
||||||
|
additional_data={
|
||||||
|
'record_id': record_id,
|
||||||
|
'user_role': session.get('role'),
|
||||||
|
'edit_reason': edit_note,
|
||||||
|
'editor_username': session.get('username')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} edited record {record_id} with no changes")
|
||||||
|
print(f"[LOG] Edit reason: {edit_note}")
|
||||||
|
|
||||||
flash(f'Attendance record for {new_employee_id} updated successfully!', 'success')
|
flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success')
|
||||||
return redirect(url_for('attendance_report'))
|
return redirect(url_for('attendance_report'))
|
||||||
|
|
||||||
# GET request - show edit form
|
# GET request - show edit form
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AttendanceData(base.db.Model):
|
|||||||
altitude = base.db.Column(base.db.Float, nullable=True)
|
altitude = base.db.Column(base.db.Float, nullable=True)
|
||||||
location_source = base.db.Column(base.db.String(50), default='manual')
|
location_source = base.db.Column(base.db.String(50), default='manual')
|
||||||
address = base.db.Column(base.db.String(500), nullable=True)
|
address = base.db.Column(base.db.String(500), nullable=True)
|
||||||
|
edit_note = base.db.Column(base.db.Text, nullable=True)
|
||||||
# Relationships
|
# Relationships
|
||||||
qr_code = base.db.relationship('QRCode', backref=base.db.backref('attendance_records', lazy='dynamic'))
|
qr_code = base.db.relationship('QRCode', backref=base.db.backref('attendance_records', lazy='dynamic'))
|
||||||
|
|
||||||
|
|||||||
+133
-37
@@ -1,9 +1,7 @@
|
|||||||
{% extends "base_authenticated.html" %} {% block title %}Edit Attendance Record
|
{% extends "base_authenticated.html" %}
|
||||||
- QR Code Management{% endblock %} {% block extra_head %}
|
{% block title %}Edit Attendance Record - QR Code Management{% endblock %}
|
||||||
<link
|
{% block extra_head %}
|
||||||
rel="stylesheet"
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/forms.css') }}" />
|
||||||
href="{{ url_for('static', filename='css/forms.css') }}"
|
|
||||||
/>
|
|
||||||
<style>
|
<style>
|
||||||
.edit-attendance-container {
|
.edit-attendance-container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
@@ -67,8 +65,61 @@
|
|||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audit-note-section {
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-note-section h3 {
|
||||||
|
color: #b45309;
|
||||||
|
border-bottom-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-note-section .form-group label {
|
||||||
|
color: #b45309;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-note-section textarea {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-note-section textarea:focus {
|
||||||
|
border-color: #d97706;
|
||||||
|
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.existing-notes {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.existing-notes h4 {
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-display textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-note-section textarea {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %} {% block content %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<div class="edit-attendance-container">
|
<div class="edit-attendance-container">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<h1>
|
<h1>
|
||||||
@@ -82,26 +133,47 @@
|
|||||||
<div class="record-info">
|
<div class="record-info">
|
||||||
<h4><i class="fas fa-info-circle"></i> Current Record Information</h4>
|
<h4><i class="fas fa-info-circle"></i> Current Record Information</h4>
|
||||||
<p><strong>Record ID:</strong> {{ attendance_record.id }}</p>
|
<p><strong>Record ID:</strong> {{ attendance_record.id }}</p>
|
||||||
<p>
|
<p><strong>Current Employee:</strong> {{ attendance_record.employee_id }}</p>
|
||||||
<strong>Current Employee:</strong> {{ attendance_record.employee_id }}
|
<p><strong>Current Date:</strong> {{ attendance_record.check_in_date.strftime('%Y-%m-%d') }}</p>
|
||||||
</p>
|
<p><strong>Current Time:</strong> {{ attendance_record.check_in_time.strftime('%H:%M') }}</p>
|
||||||
<p>
|
<p><strong>Current Location:</strong> {{ attendance_record.location_name }}</p>
|
||||||
<strong>Current Date:</strong> {{
|
|
||||||
attendance_record.check_in_date.strftime('%Y-%m-%d') }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Current Time:</strong> {{
|
|
||||||
attendance_record.check_in_time.strftime('%H:%M') }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Current Location:</strong> {{ attendance_record.location_name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form method="POST" action="{{ url_for('edit_attendance', record_id=attendance_record.id) }}">
|
||||||
method="POST"
|
|
||||||
action="{{ url_for('edit_attendance', record_id=attendance_record.id) }}"
|
<!-- Audit Note Section -->
|
||||||
>
|
<div class="audit-note-section">
|
||||||
|
<h3><i class="fas fa-clipboard-list"></i> Audit Information</h3>
|
||||||
|
|
||||||
|
{% if attendance_record.edit_note %}
|
||||||
|
<div class="existing-notes">
|
||||||
|
<h4><i class="fas fa-history"></i> Previous Edit History</h4>
|
||||||
|
<div class="notes-display">
|
||||||
|
<textarea readonly class="form-control" rows="4" style="background-color: #f8f9fa; font-family: monospace; font-size: 0.9em;">{{ attendance_record.edit_note }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top: 1rem;">
|
||||||
|
<label for="edit_note">{% if attendance_record.edit_note %}New Edit Reason (Required) *{% else %}Reason for Edit (Required) *{% endif %}</label>
|
||||||
|
<textarea
|
||||||
|
id="edit_note"
|
||||||
|
name="edit_note"
|
||||||
|
required
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Please provide a reason for editing this attendance record (for audit purposes)"
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">
|
||||||
|
{% if attendance_record.edit_note %}
|
||||||
|
<strong>Note:</strong> Your new reason will be appended to the existing audit trail.
|
||||||
|
{% else %}
|
||||||
|
This note will be logged for audit purposes and added to the record's audit trail.
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Employee Information -->
|
<!-- Employee Information -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3><i class="fas fa-user"></i> Employee Information</h3>
|
<h3><i class="fas fa-user"></i> Employee Information</h3>
|
||||||
@@ -178,26 +250,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block extra_scripts %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Form validation
|
// Form validation
|
||||||
const form = document.querySelector("form");
|
const form = document.querySelector("form");
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
const employeeId = document.getElementById("employee_id").value.trim();
|
const employeeId = document.getElementById("employee_id").value.trim();
|
||||||
const locationName = document
|
const locationName = document.getElementById("location_name").value.trim();
|
||||||
.getElementById("location_name")
|
const editNote = document.getElementById("edit_note").value.trim();
|
||||||
.value.trim();
|
|
||||||
|
|
||||||
if (!employeeId || !locationName) {
|
if (!employeeId || !locationName || !editNote) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert("Please fill in all required fields.");
|
alert("Please fill in all required fields, including the audit note.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm update
|
// Confirm update
|
||||||
const confirmUpdate = confirm(
|
const confirmUpdate = confirm(
|
||||||
"Are you sure you want to update this attendance record?"
|
"Are you sure you want to update this attendance record?\n\nThis action will be logged for audit purposes."
|
||||||
);
|
);
|
||||||
if (!confirmUpdate) {
|
if (!confirmUpdate) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -205,11 +278,34 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Auto-uppercase employee ID
|
// Auto-uppercase employee ID
|
||||||
document
|
document.getElementById("employee_id").addEventListener("input", function () {
|
||||||
.getElementById("employee_id")
|
this.value = this.value.toUpperCase();
|
||||||
.addEventListener("input", function () {
|
});
|
||||||
this.value = this.value.toUpperCase();
|
|
||||||
});
|
// Character counter for audit note
|
||||||
|
const noteTextarea = document.getElementById("edit_note");
|
||||||
|
const maxLength = 500;
|
||||||
|
|
||||||
|
// Add character counter
|
||||||
|
const charCounter = document.createElement("small");
|
||||||
|
charCounter.className = "form-help";
|
||||||
|
charCounter.style.float = "right";
|
||||||
|
charCounter.style.color = "#6b7280";
|
||||||
|
noteTextarea.parentNode.appendChild(charCounter);
|
||||||
|
|
||||||
|
function updateCharCounter() {
|
||||||
|
const remaining = maxLength - noteTextarea.value.length;
|
||||||
|
charCounter.textContent = `${remaining} characters remaining`;
|
||||||
|
if (remaining < 50) {
|
||||||
|
charCounter.style.color = "#ef4444";
|
||||||
|
} else {
|
||||||
|
charCounter.style.color = "#6b7280";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noteTextarea.setAttribute("maxlength", maxLength);
|
||||||
|
noteTextarea.addEventListener("input", updateCharCounter);
|
||||||
|
updateCharCounter();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user