539 lines
18 KiB
HTML
539 lines
18 KiB
HTML
{% extends "base_authenticated.html" %}
|
|
{% block title %}Edit Attendance Record - QR Code Management{% endblock %}
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/forms.css') }}" />
|
|
<style>
|
|
.edit-attendance-container {
|
|
max-width: 800px;
|
|
margin: 2rem auto;
|
|
padding: 2rem;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.form-header {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 2px solid #e5e7eb;
|
|
}
|
|
|
|
.form-header h1 {
|
|
color: #1f2937;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.form-header p {
|
|
color: #6b7280;
|
|
margin: 0;
|
|
}
|
|
|
|
.form-section {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.form-section h3 {
|
|
color: #374151;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
margin-top: 2rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.record-info {
|
|
background: #f9fafb;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.record-info h4 {
|
|
color: #374151;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.record-info p {
|
|
color: #6b7280;
|
|
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>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="edit-attendance-container">
|
|
<div class="form-header">
|
|
<h1>
|
|
<i class="fas fa-edit"></i>
|
|
Edit Attendance Record
|
|
</h1>
|
|
<p>Modify attendance record details (Admin & Accounting Access)</p>
|
|
</div>
|
|
|
|
<!-- Current Record Info -->
|
|
<div class="record-info">
|
|
<h4><i class="fas fa-info-circle"></i> Current Record Information</h4>
|
|
<p><strong>Record ID:</strong> {{ attendance_record.id }}</p>
|
|
<p><strong>Current Employee:</strong> {{ attendance_record.employee_id }}</p>
|
|
<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>
|
|
<p><strong>Current Event Type:</strong>
|
|
<span style="padding: 0.25rem 0.5rem; background-color: {% if attendance_record.qr_code and attendance_record.qr_code.location_event == 'Check In' %}#d1fae5{% else %}#fef3c7{% endif %}; border-radius: 4px; font-weight: 600;">
|
|
{{ attendance_record.qr_code.location_event if attendance_record.qr_code else 'Unknown' }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ url_for('attendance.edit_attendance', record_id=attendance_record.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
|
|
<!-- 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 -->
|
|
<div class="form-section">
|
|
<h3><i class="fas fa-user"></i> Employee Information</h3>
|
|
<div class="form-group">
|
|
<label for="employee_id">Employee ID *</label>
|
|
<input
|
|
type="text"
|
|
id="employee_id"
|
|
name="employee_id"
|
|
value="{{ attendance_record.employee_id }}"
|
|
required
|
|
class="form-control"
|
|
placeholder="Enter employee ID"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date & Time Information -->
|
|
<div class="form-section">
|
|
<h3><i class="fas fa-clock"></i> Check-in Details</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="check_in_date">Check-in Date *</label>
|
|
<input
|
|
type="date"
|
|
id="check_in_date"
|
|
name="check_in_date"
|
|
value="{{ attendance_record.check_in_date.strftime('%Y-%m-%d') }}"
|
|
required
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="check_in_time">Check-in Time *</label>
|
|
<input
|
|
type="time"
|
|
id="check_in_time"
|
|
name="check_in_time"
|
|
value="{{ attendance_record.check_in_time.strftime('%H:%M') }}"
|
|
required
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location Information -->
|
|
<div class="form-section">
|
|
<h3><i class="fas fa-map-marker-alt"></i> Location & Event Information</h3>
|
|
|
|
<!-- Project Selection -->
|
|
<div class="form-group">
|
|
<label for="project_id">Project *</label>
|
|
<select id="project_id" class="form-control" required>
|
|
<option value="">-- Select Project --</option>
|
|
{% for project in projects %}
|
|
<option value="{{ project.id }}"
|
|
{% if attendance_record.qr_code and attendance_record.qr_code.project_id == project.id %}selected{% endif %}>
|
|
{{ project.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Location Selection -->
|
|
<div class="form-group">
|
|
<label for="location_select">Location *</label>
|
|
<select id="location_select" class="form-control" required disabled>
|
|
<option value="">-- Select Project First --</option>
|
|
</select>
|
|
<div id="location_loading" style="display: none; text-align: center; padding: 0.5rem; color: #007bff;">
|
|
<i class="fas fa-spinner fa-spin"></i> Loading locations...
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Type Selection -->
|
|
<div class="form-group">
|
|
<label for="event_type_select">Event Type *</label>
|
|
<select id="event_type_select" class="form-control" required disabled>
|
|
<option value="">-- Select Location First --</option>
|
|
</select>
|
|
<small class="form-help">Select whether this is a Check In or Check Out event</small>
|
|
</div>
|
|
|
|
<!-- Hidden field for QR code ID -->
|
|
<input type="hidden" id="qr_code_id" name="qr_code_id" required>
|
|
|
|
<!-- Location Name (auto-filled, read-only) -->
|
|
<div class="form-group">
|
|
<label for="location_name">Location Name (Auto-filled) *</label>
|
|
<input
|
|
type="text"
|
|
id="location_name"
|
|
name="location_name"
|
|
value="{{ attendance_record.location_name }}"
|
|
required
|
|
class="form-control"
|
|
readonly
|
|
style="background-color: #f3f4f6;"
|
|
/>
|
|
<small class="form-help">This field is automatically updated based on your location selection above</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Read-Only Notice for Non-Authorized Users -->
|
|
{% if session.role not in ['admin', 'accounting'] %}
|
|
<div class="alert alert-warning" style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<strong>Read-Only Access:</strong> You can view this record but cannot make changes. Only Admin and Accounting staff can modify attendance records.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
{% if session.role in ['admin', 'accounting'] %}
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save"></i>
|
|
Update Record
|
|
</button>
|
|
{% endif %}
|
|
<a href="{{ url_for('attendance.attendance_report') }}" class="btn btn-secondary">
|
|
<i class="fas fa-times"></i>
|
|
Cancel
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const userRole = '{{ session.role }}';
|
|
const authorizedRoles = ['admin', 'accounting'];
|
|
|
|
// Store location data for the dropdowns
|
|
let locationData = {};
|
|
|
|
// Get form elements
|
|
const projectSelect = document.getElementById('project_id');
|
|
const locationSelect = document.getElementById('location_select');
|
|
const eventTypeSelect = document.getElementById('event_type_select');
|
|
const qrCodeIdHidden = document.getElementById('qr_code_id');
|
|
const locationNameInput = document.getElementById('location_name');
|
|
const locationLoading = document.getElementById('location_loading');
|
|
|
|
// Current record data
|
|
const currentQrCodeId = {{ attendance_record.qr_code_id }};
|
|
const currentLocationName = "{{ attendance_record.location_name }}";
|
|
const currentProjectId = {{ attendance_record.qr_code.project_id if attendance_record.qr_code and attendance_record.qr_code.project_id else 'null' }};
|
|
const currentLocationEvent = "{{ attendance_record.qr_code.location_event if attendance_record.qr_code else '' }}";
|
|
|
|
// Project selection handler
|
|
projectSelect.addEventListener('change', function() {
|
|
const projectId = this.value;
|
|
|
|
// Reset dependent fields
|
|
locationSelect.innerHTML = '<option value="">-- Select Location --</option>';
|
|
locationSelect.disabled = true;
|
|
eventTypeSelect.innerHTML = '<option value="">-- Select Location First --</option>';
|
|
eventTypeSelect.disabled = true;
|
|
qrCodeIdHidden.value = '';
|
|
locationData = {};
|
|
|
|
if (!projectId) {
|
|
locationSelect.innerHTML = '<option value="">-- Select Project First --</option>';
|
|
return;
|
|
}
|
|
|
|
// Load locations for selected project
|
|
locationLoading.style.display = 'block';
|
|
|
|
fetch(`/api/get_project_locations?project_id=${projectId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
locationLoading.style.display = 'none';
|
|
|
|
if (data.success && data.locations.length > 0) {
|
|
locationSelect.disabled = false;
|
|
|
|
data.locations.forEach((loc, index) => {
|
|
const locationKey = `loc_${index}`;
|
|
locationData[locationKey] = {
|
|
location: loc.location,
|
|
location_address: loc.location_address,
|
|
qr_codes: loc.qr_codes
|
|
};
|
|
|
|
const option = document.createElement('option');
|
|
option.value = locationKey;
|
|
option.textContent = `${loc.location} - ${loc.location_address}`;
|
|
|
|
// Pre-select if this matches current location
|
|
if (loc.location === currentLocationName && currentProjectId == projectId) {
|
|
option.selected = true;
|
|
}
|
|
|
|
locationSelect.appendChild(option);
|
|
});
|
|
|
|
// Trigger change to populate event types if location was pre-selected
|
|
if (locationSelect.value) {
|
|
locationSelect.dispatchEvent(new Event('change'));
|
|
}
|
|
} else {
|
|
locationSelect.innerHTML = '<option value="">No active locations found for this project</option>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
locationLoading.style.display = 'none';
|
|
console.error('Error loading locations:', error);
|
|
alert('Error loading locations. Please try again.');
|
|
});
|
|
});
|
|
|
|
// Location selection handler
|
|
locationSelect.addEventListener('change', function() {
|
|
const locationKey = this.value;
|
|
|
|
eventTypeSelect.innerHTML = '';
|
|
eventTypeSelect.disabled = false;
|
|
qrCodeIdHidden.value = '';
|
|
|
|
if (locationKey && locationData[locationKey]) {
|
|
const location = locationData[locationKey];
|
|
const qrCodes = location.qr_codes;
|
|
|
|
// Update location name field
|
|
locationNameInput.value = location.location;
|
|
|
|
// Add available event types
|
|
if (qrCodes['Check In']) {
|
|
const option = document.createElement('option');
|
|
option.value = qrCodes['Check In'];
|
|
option.textContent = 'Check In';
|
|
|
|
// Pre-select if this matches current event
|
|
if (currentLocationEvent === 'Check In' && currentQrCodeId == qrCodes['Check In']) {
|
|
option.selected = true;
|
|
}
|
|
|
|
eventTypeSelect.appendChild(option);
|
|
}
|
|
|
|
if (qrCodes['Check Out']) {
|
|
const option = document.createElement('option');
|
|
option.value = qrCodes['Check Out'];
|
|
option.textContent = 'Check Out';
|
|
|
|
// Pre-select if this matches current event
|
|
if (currentLocationEvent === 'Check Out' && currentQrCodeId == qrCodes['Check Out']) {
|
|
option.selected = true;
|
|
}
|
|
|
|
eventTypeSelect.appendChild(option);
|
|
}
|
|
|
|
if (eventTypeSelect.options.length === 0) {
|
|
eventTypeSelect.innerHTML = '<option value="">No event types available</option>';
|
|
eventTypeSelect.disabled = true;
|
|
} else {
|
|
// Set hidden field to selected or first option
|
|
qrCodeIdHidden.value = eventTypeSelect.value || eventTypeSelect.options[0].value;
|
|
}
|
|
} else {
|
|
eventTypeSelect.innerHTML = '<option value="">-- Select Location First --</option>';
|
|
eventTypeSelect.disabled = true;
|
|
}
|
|
});
|
|
|
|
// Event type selection handler
|
|
eventTypeSelect.addEventListener('change', function() {
|
|
qrCodeIdHidden.value = this.value;
|
|
});
|
|
|
|
// Initialize by triggering project change if project is already selected
|
|
if (currentProjectId && projectSelect.value) {
|
|
projectSelect.dispatchEvent(new Event('change'));
|
|
}
|
|
|
|
// Disable all form inputs for non-authorized roles
|
|
if (!authorizedRoles.includes(userRole)) {
|
|
const formInputs = document.querySelectorAll('input, textarea, select, button[type="submit"]');
|
|
formInputs.forEach(input => {
|
|
if (input.type !== 'button' && !input.closest('a')) {
|
|
input.disabled = true;
|
|
input.style.backgroundColor = '#f3f4f6';
|
|
input.style.cursor = 'not-allowed';
|
|
}
|
|
});
|
|
|
|
// Prevent form submission
|
|
const form = document.querySelector("form");
|
|
form.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
alert("You do not have permission to modify this record. Only Admin and Accounting staff can make changes.");
|
|
return false;
|
|
});
|
|
|
|
return; // Exit early for non-authorized users
|
|
}
|
|
|
|
// Form validation (only for authorized users)
|
|
const form = document.querySelector("form");
|
|
form.addEventListener("submit", function (e) {
|
|
const employeeId = document.getElementById("employee_id").value.trim();
|
|
const locationName = locationNameInput.value.trim();
|
|
const editNote = document.getElementById("edit_note").value.trim();
|
|
const qrCodeId = qrCodeIdHidden.value.trim();
|
|
|
|
if (!employeeId || !locationName || !editNote || !qrCodeId) {
|
|
e.preventDefault();
|
|
alert("Please fill in all required fields, including selecting the project, location, and event type.");
|
|
return;
|
|
}
|
|
|
|
// Confirm update
|
|
const confirmUpdate = confirm(
|
|
"Are you sure you want to update this attendance record?\n\nThis action will be logged for audit purposes."
|
|
);
|
|
if (!confirmUpdate) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Auto-uppercase employee ID
|
|
document.getElementById("employee_id").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>
|
|
{% endblock %} |