Updated TA export function

This commit is contained in:
2025-10-08 16:30:40 -04:00
parent 8fdb21d2ec
commit cfb681f503
3 changed files with 680 additions and 336 deletions
+321 -5
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, send_file from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, send_file, Response
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -6541,7 +6541,7 @@ def time_attendance_dashboard():
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
@app.route('/time-attendance/import', methods=['GET', 'POST']) @app.route('/time-attendance/import', methods=['GET', 'POST'])
@admin_required @login_required
@log_database_operations('time_attendance_import') @log_database_operations('time_attendance_import')
def import_time_attendance(): def import_time_attendance():
"""Enhanced import with duplicate review""" """Enhanced import with duplicate review"""
@@ -6678,7 +6678,7 @@ def import_time_attendance():
@app.route('/time-attendance/import/analyze-duplicates', methods=['POST']) @app.route('/time-attendance/import/analyze-duplicates', methods=['POST'])
@admin_required @login_required
def analyze_import_duplicates(): def analyze_import_duplicates():
"""AJAX endpoint to analyze file for duplicates""" """AJAX endpoint to analyze file for duplicates"""
try: try:
@@ -6744,7 +6744,7 @@ def analyze_import_duplicates():
@app.route('/time-attendance/import/cancel-pending') @app.route('/time-attendance/import/cancel-pending')
@admin_required @login_required
def cancel_pending_import(): def cancel_pending_import():
"""Cancel pending import and cleanup temp file""" """Cancel pending import and cleanup temp file"""
try: try:
@@ -6766,7 +6766,7 @@ def cancel_pending_import():
@app.route('/time-attendance/import/validate', methods=['POST']) @app.route('/time-attendance/import/validate', methods=['POST'])
@admin_required @login_required
def validate_import_file(): def validate_import_file():
"""AJAX endpoint to validate Excel file before import""" """AJAX endpoint to validate Excel file before import"""
try: try:
@@ -6968,6 +6968,322 @@ def download_import_template():
flash('Error generating template file.', 'error') flash('Error generating template file.', 'error')
return redirect(url_for('import_time_attendance')) return redirect(url_for('import_time_attendance'))
@app.route('/time-attendance/export')
@login_required
@log_user_activity('time_attendance_export')
def export_time_attendance():
"""Export time attendance records to CSV or Excel"""
try:
# Get export format (default to CSV)
export_format = request.args.get('format', 'csv').lower()
# Get filter parameters (same as records page)
employee_filter = request.args.get('employee_id')
location_filter = request.args.get('location_name')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
import_batch = request.args.get('import_batch')
# Build query with same filters as the view
from models.time_attendance import TimeAttendance
query = TimeAttendance.query
# Apply filters
if employee_filter:
query = query.filter(TimeAttendance.employee_id == employee_filter)
if location_filter:
query = query.filter(TimeAttendance.location_name == location_filter)
if start_date:
try:
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
query = query.filter(TimeAttendance.attendance_date >= start_date_obj)
except ValueError:
flash('Invalid start date format.', 'error')
return redirect(url_for('time_attendance_records'))
if end_date:
try:
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
query = query.filter(TimeAttendance.attendance_date <= end_date_obj)
except ValueError:
flash('Invalid end date format.', 'error')
return redirect(url_for('time_attendance_records'))
if import_batch:
query = query.filter(TimeAttendance.import_batch_id == import_batch)
# Order by date and time (most recent first)
records = query.order_by(
TimeAttendance.attendance_date.desc(),
TimeAttendance.attendance_time.desc()
).all()
if not records:
flash('No records found to export.', 'warning')
return redirect(url_for('time_attendance_records'))
# Log export
logger_handler.logger.info(
f"User {session['username']} exported {len(records)} time attendance records "
f"in {export_format.upper()} format"
)
# Generate filename
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filter_desc = []
if employee_filter:
filter_desc.append(f"emp_{employee_filter}")
if location_filter:
filter_desc.append(f"loc_{location_filter[:10]}")
if start_date:
filter_desc.append(f"from_{start_date}")
if end_date:
filter_desc.append(f"to_{end_date}")
filter_str = "_".join(filter_desc) if filter_desc else "all"
# Export based on format
if export_format == 'excel' or export_format == 'xlsx':
return export_time_attendance_excel(records, timestamp, filter_str)
else:
return export_time_attendance_csv(records, timestamp, filter_str)
except Exception as e:
logger_handler.logger.error(f"Error exporting time attendance records: {e}")
flash('Error generating export file. Please try again.', 'error')
return redirect(url_for('time_attendance_records'))
def export_time_attendance_csv(records, timestamp, filter_str):
"""Generate CSV export of time attendance records"""
import csv
import io
# Create CSV in memory
output = io.StringIO()
writer = csv.writer(output)
# Write header
writer.writerow([
'ID',
'Employee ID',
'Employee Name',
'Platform',
'Date',
'Time',
'Location Name',
'Action Description',
'Event Description',
'Recorded Address',
'Import Batch ID',
'Import Date',
'Import Source',
'Created Date'
])
# Write data rows
for record in records:
writer.writerow([
record.id,
record.employee_id,
record.employee_name,
record.platform or '',
record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else '',
record.attendance_time.strftime('%H:%M:%S') if record.attendance_time else '',
record.location_name,
record.action_description,
record.event_description or '',
record.recorded_address or '',
record.import_batch_id or '',
record.import_date.strftime('%Y-%m-%d %H:%M:%S') if record.import_date else '',
record.import_source or '',
record.created_date.strftime('%Y-%m-%d %H:%M:%S') if record.created_date else ''
])
# Prepare response
output.seek(0)
filename = f'time_attendance_{filter_str}_{timestamp}.csv'
return Response(
output.getvalue(),
mimetype='text/csv',
headers={
'Content-Disposition': f'attachment; filename={filename}'
}
)
def export_time_attendance_excel(records, timestamp, filter_str):
"""Generate Excel export of time attendance records"""
import io
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# Create workbook
wb = Workbook()
ws = wb.active
ws.title = "Time Attendance Records"
# Define styles
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=11)
border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# Headers
headers = [
'ID', 'Employee ID', 'Employee Name', 'Platform', 'Date', 'Time',
'Location Name', 'Action Description', 'Event Description',
'Recorded Address', 'Import Batch ID', 'Import Date', 'Import Source'
]
# Write headers
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num)
cell.value = header
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal='center', vertical='center')
cell.border = border
# Write data
for row_num, record in enumerate(records, 2):
data = [
record.id,
record.employee_id,
record.employee_name,
record.platform or '',
record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else '',
record.attendance_time.strftime('%H:%M:%S') if record.attendance_time else '',
record.location_name,
record.action_description,
record.event_description or '',
record.recorded_address or '',
record.import_batch_id or '',
record.import_date.strftime('%Y-%m-%d %H:%M:%S') if record.import_date else '',
record.import_source or ''
]
for col_num, value in enumerate(data, 1):
cell = ws.cell(row=row_num, column=col_num)
cell.value = value
cell.border = border
# Special formatting for specific columns
if col_num in [5, 6]: # Date and Time columns
cell.alignment = Alignment(horizontal='center')
elif col_num in [1, 2]: # ID columns
cell.alignment = Alignment(horizontal='right')
# Adjust column widths
column_widths = {
'A': 8, # ID
'B': 12, # Employee ID
'C': 20, # Employee Name
'D': 15, # Platform
'E': 12, # Date
'F': 10, # Time
'G': 20, # Location Name
'H': 18, # Action Description
'I': 25, # Event Description
'J': 30, # Recorded Address
'K': 15, # Import Batch ID
'L': 18, # Import Date
'M': 25 # Import Source
}
for col, width in column_widths.items():
ws.column_dimensions[col].width = width
# Freeze header row
ws.freeze_panes = 'A2'
# Add summary sheet
ws_summary = wb.create_sheet("Summary")
# Summary statistics
total_records = len(records)
unique_employees = len(set(r.employee_id for r in records))
unique_locations = len(set(r.location_name for r in records))
date_range_start = min(r.attendance_date for r in records if r.attendance_date)
date_range_end = max(r.attendance_date for r in records if r.attendance_date)
# Action breakdown
action_counts = {}
for record in records:
action = record.action_description
action_counts[action] = action_counts.get(action, 0) + 1
# Write summary
summary_data = [
['Time Attendance Export Summary'],
[''],
['Export Date:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
['Exported By:', session.get('username', 'Unknown')],
[''],
['Statistics'],
['Total Records:', total_records],
['Unique Employees:', unique_employees],
['Unique Locations:', unique_locations],
['Date Range:', f"{date_range_start.strftime('%Y-%m-%d')} to {date_range_end.strftime('%Y-%m-%d')}"],
[''],
['Actions Breakdown']
]
for action, count in sorted(action_counts.items()):
summary_data.append([action, count])
for row_num, row_data in enumerate(summary_data, 1):
for col_num, value in enumerate(row_data, 1):
cell = ws_summary.cell(row=row_num, column=col_num)
cell.value = value
# Style the title
if row_num == 1:
cell.font = Font(bold=True, size=14)
elif row_num in [6, 12]: # Section headers
cell.font = Font(bold=True, size=11)
# Adjust summary column widths
ws_summary.column_dimensions['A'].width = 25
ws_summary.column_dimensions['B'].width = 15
# Save to bytes
output = io.BytesIO()
wb.save(output)
output.seek(0)
filename = f'time_attendance_{filter_str}_{timestamp}.xlsx'
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=filename
)
@app.route('/time-attendance/export/quick-csv')
@login_required
@log_user_activity('time_attendance_quick_csv_export')
def quick_csv_export_time_attendance():
"""Quick CSV export with current page filters"""
# Redirect to main export with CSV format
return redirect(url_for('export_time_attendance', format='csv', **request.args))
@app.route('/time-attendance/export/excel')
@login_required
@log_user_activity('time_attendance_excel_export')
def excel_export_time_attendance():
"""Excel export with current page filters"""
# Redirect to main export with Excel format
return redirect(url_for('export_time_attendance', format='excel', **request.args))
@app.route('/time-attendance/records') @app.route('/time-attendance/records')
@login_required @login_required
@log_user_activity('time_attendance_records_view') @log_user_activity('time_attendance_records_view')
+107 -8
View File
@@ -111,6 +111,105 @@
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Fixed styles for instructions section */
.instructions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.instruction-item {
text-align: center;
padding: 1.5rem;
background: #f8fafc;
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
}
.instruction-icon {
width: 60px;
height: 60px;
margin: 0 auto 1rem;
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 1.5rem;
}
.instruction-item h3 {
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 0.5rem;
}
.instruction-item p {
color: #64748b;
font-size: 0.875rem;
}
.sample-format {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
padding: 1.5rem;
margin-top: 2rem;
}
.sample-format h4 {
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.format-table {
overflow-x: auto;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.format-table table {
width: 100%;
border-collapse: collapse;
background: white;
font-size: 0.875rem;
}
.format-table thead {
background: linear-gradient(135deg, #4299e1, #3182ce);
color: white;
}
.format-table th {
padding: 0.75rem 1rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #2b6cb0;
white-space: nowrap;
}
.format-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e2e8f0;
color: #4a5568;
}
.format-table tbody tr:last-child td {
border-bottom: none;
}
.format-table tbody tr:hover {
background: #f7fafc;
}
</style> </style>
{% endblock %} {% endblock %}
@@ -277,14 +376,6 @@
Import Options Import Options
</h3> </h3>
<div class="checkbox-group"> <div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="skip_duplicates" name="skip_duplicates" value="true" checked>
<label for="skip_duplicates" class="checkbox-label">
<strong>Skip Duplicate Records</strong>
<span>Automatically detect and skip records that already exist in the system</span>
</label>
</div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="analyze_duplicates" name="analyze_duplicates" value="true"> <input type="checkbox" id="analyze_duplicates" name="analyze_duplicates" value="true">
<label for="analyze_duplicates" class="checkbox-label"> <label for="analyze_duplicates" class="checkbox-label">
@@ -293,6 +384,14 @@
</label> </label>
</div> </div>
<div class="checkbox-item">
<input type="checkbox" id="skip_duplicates" name="skip_duplicates" value="true" checked>
<label for="skip_duplicates" class="checkbox-label">
<strong>Skip Duplicate Records</strong>
<span>Automatically detect and skip records that already exist in the system</span>
</label>
</div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="validate_only" name="validate_only" value="true"> <input type="checkbox" id="validate_only" name="validate_only" value="true">
<label for="validate_only" class="checkbox-label"> <label for="validate_only" class="checkbox-label">
+250 -321
View File
@@ -3,6 +3,94 @@
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/time_attendance.css') }}">
<style>
/* Export dropdown styles */
.export-dropdown {
position: relative;
display: inline-block;
}
.export-menu {
display: none;
position: absolute;
right: 0;
top: 100%;
margin-top: 0.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
z-index: 9999;
min-width: 220px;
}
.export-menu.show {
display: block;
}
.export-menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
transition: background 0.2s;
font-size: 0.875rem;
color: #2d3748;
}
.export-menu-item:first-child {
border-radius: 8px 8px 0 0;
}
.export-menu-item:last-child {
border-radius: 0 0 8px 8px;
}
.export-menu-item:hover {
background: #f7fafc;
}
.export-menu-item i {
font-size: 1.125rem;
width: 20px;
text-align: center;
}
.export-menu-item .fa-file-csv {
color: #4299e1;
}
.export-menu-item .fa-file-excel {
color: #48bb78;
}
.export-menu-divider {
height: 1px;
background: #e2e8f0;
margin: 0.25rem 0;
}
.export-menu-info {
padding: 0.75rem 1rem;
font-size: 0.75rem;
color: #718096;
background: #f7fafc;
border-radius: 0 0 8px 8px;
}
.time-attendance-page {
overflow: visible !important;
}
.time-attendance-header {
overflow: visible !important;
}
</style>
{% endblock %} {% endblock %}
{% block page_title %}Time Attendance Records{% endblock %} {% block page_title %}Time Attendance Records{% endblock %}
@@ -35,10 +123,40 @@
Import Data Import Data
</a> </a>
{% endif %} {% endif %}
<button class="btn btn-secondary" onclick="exportRecords()">
<i class="fas fa-download"></i> <!-- Export Dropdown -->
Export <div class="export-dropdown">
</button> <button class="btn btn-secondary" onclick="toggleExportMenu()">
<i class="fas fa-download"></i>
Export
<i class="fas fa-chevron-down" style="margin-left: 0.5rem; font-size: 0.75rem;"></i>
</button>
<div class="export-menu" id="exportMenu">
<button class="export-menu-item" onclick="exportToCSV()">
<i class="fas fa-file-csv"></i>
<div>
<strong>Export to CSV</strong>
<div style="font-size: 0.75rem; color: #718096;">Quick download, all data</div>
</div>
</button>
<button class="export-menu-item" onclick="exportToExcel()">
<i class="fas fa-file-excel"></i>
<div>
<strong>Export to Excel</strong>
<div style="font-size: 0.75rem; color: #718096;">Formatted with summary sheet</div>
</div>
</button>
<div class="export-menu-divider"></div>
<div class="export-menu-info">
<i class="fas fa-info-circle"></i>
Export will include current filters
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -99,117 +217,84 @@
value="{{ current_filters.end_date or '' }}"> value="{{ current_filters.end_date or '' }}">
</div> </div>
<div class="form-group"> <div class="form-actions">
<label class="form-label">&nbsp;</label> <button type="submit" class="btn btn-primary">
<div class="filter-actions"> <i class="fas fa-search"></i>
<button type="submit" class="btn btn-primary"> Apply Filters
<i class="fas fa-search"></i> </button>
Apply Filters <a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
</button> <i class="fas fa-undo"></i>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline"> Clear Filters
<i class="fas fa-times"></i> </a>
Clear
</a>
</div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Records Table --> <!-- Records Count Info -->
<div class="records-section"> {% if records %}
<div class="records-header"> <div class="records-info">
<h2> <div class="info-card">
<i class="fas fa-table"></i> <i class="fas fa-database"></i>
Attendance Records <span>Showing {{ records.items|length }} of {{ records.total }} records</span>
</h2> {% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
<div class="records-meta"> <span class="filter-badge">Filtered</span>
{% if records.items %} {% endif %}
Showing {{ records.per_page * (records.page - 1) + 1 }} -
{{ records.per_page * (records.page - 1) + records.items|length }}
of {{ records.total }} records
{% else %}
No records found
{% endif %}
</div>
</div> </div>
</div>
{% endif %}
{% if records.items %} <!-- Records Table -->
<div class="records-table-container"> {% if records and records.items %}
<div class="table-section">
<div class="table-container">
<table class="records-table"> <table class="records-table">
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Employee</th> <th>Employee</th>
<th>Date & Time</th> <th>Date</th>
<th>Action</th> <th>Time</th>
<th>Location</th> <th>Location</th>
<th>Action</th>
<th>Platform</th> <th>Platform</th>
<th>Address</th> <th>Import Source</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for record in records.items %} {% for record in records.items %}
<tr> <tr>
<td>{{ record.id }}</td>
<td> <td>
<div class="employee-cell"> <div class="employee-info">
<div class="employee-avatar"> <strong>{{ record.employee_name }}</strong>
{{ record.employee_name[0].upper() if record.employee_name else record.employee_id[0].upper() }} <small>ID: {{ record.employee_id }}</small>
</div>
<div class="employee-info">
<div class="employee-name">{{ record.employee_name }}</div>
<div class="employee-id">ID: {{ record.employee_id }}</div>
</div>
</div>
</td>
<td class="datetime-cell">
<div class="datetime-info">
<div class="date">{{ record.attendance_date.strftime('%Y-%m-%d') }}</div>
<div class="time">{{ record.attendance_time.strftime('%H:%M:%S') }}</div>
</div> </div>
</td> </td>
<td>{{ record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else 'N/A' }}</td>
<td>{{ record.attendance_time.strftime('%H:%M:%S') if record.attendance_time else 'N/A' }}</td>
<td>{{ record.location_name }}</td>
<td> <td>
<span class="action-badge {{ record.action_description.lower().replace(' ', '-') }}"> <span class="action-badge {% if 'Check In' in record.action_description %}check-in{% else %}check-out{% endif %}">
{% if record.action_description.lower() == 'check in' %}
<i class="fas fa-sign-in-alt"></i>
{% elif record.action_description.lower() == 'check out' %}
<i class="fas fa-sign-out-alt"></i>
{% else %}
<i class="fas fa-clock"></i>
{% endif %}
{{ record.action_description }} {{ record.action_description }}
</span> </span>
</td> </td>
<td class="location-cell"> <td>{{ record.platform or 'N/A' }}</td>
<div class="location-info"> <td>
<i class="fas fa-map-marker-alt"></i> <div class="import-info">
{{ record.location_name }} {{ record.import_source or 'Unknown' }}
<small>{{ record.import_date.strftime('%Y-%m-%d') if record.import_date else '' }}</small>
</div> </div>
</td> </td>
<td class="platform-cell"> <td>
{{ record.platform or 'Unknown' }} <div class="record-actions">
</td> <button class="action-btn btn-view" onclick="viewRecord({{ record.id }})" title="View Details">
<td class="address-cell">
{% if record.recorded_address %}
<span title="{{ record.recorded_address }}">
{{ record.recorded_address[:30] }}{% if record.recorded_address|length > 30 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">No address</span>
{% endif %}
</td>
<td class="actions-cell">
<div class="action-buttons">
<a href="{{ url_for('time_attendance_record_detail', record_id=record.id) }}"
class="action-btn view">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
View </button>
</a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<button class="action-btn delete" <button class="action-btn btn-delete" onclick="confirmDelete({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date.strftime('%Y-%m-%d') if record.attendance_date else 'N/A' }}')" title="Delete">
onclick="confirmDelete({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date }}')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
Delete
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -268,37 +353,40 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
{% else %} {% else %}
<!-- Empty State --> <!-- Empty State -->
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon"> <div class="empty-icon">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</div> </div>
<h3>No Records Found</h3> <h3>No Records Found</h3>
<p> <p>
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %} {% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
No attendance records match your current filter criteria. No attendance records match your current filter criteria.
Try adjusting your filters or clearing them to see all records. Try adjusting your filters or clearing them to see all records.
{% else %} {% else %}
No time attendance records have been imported yet. No time attendance records have been imported yet.
Import your first Excel file to get started. Import your first Excel file to get started.
{% endif %} {% endif %}
</p> </p>
<div class="empty-actions">
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %} {% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary">
<i class="fas fa-times"></i> <i class="fas fa-undo"></i>
Clear Filters Clear Filters
</a> </a>
{% elif session.role == 'admin' %} {% endif %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> {% if session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-secondary">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
Import Records Import Data
</a> </a>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
@@ -306,7 +394,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3> <h3>
<i class="fas fa-exclamation-triangle text-danger"></i> <i class="fas fa-exclamation-triangle"></i>
Confirm Deletion Confirm Deletion
</h3> </h3>
<button class="modal-close" onclick="closeDeleteModal()"> <button class="modal-close" onclick="closeDeleteModal()">
@@ -315,14 +403,9 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Are you sure you want to delete this attendance record?</p> <p>Are you sure you want to delete this attendance record?</p>
<div class="record-details"> <p><strong>Employee:</strong> <span id="deleteEmployeeName"></span></p>
<div><strong>Employee:</strong> <span id="deleteEmployeeName"></span></div> <p><strong>Date:</strong> <span id="deleteDate"></span></p>
<div><strong>Date:</strong> <span id="deleteDate"></span></div> <p class="text-danger">This action cannot be undone.</p>
</div>
<p class="warning-text">
<i class="fas fa-exclamation-triangle"></i>
This action cannot be undone.
</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button> <button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>
@@ -336,207 +419,8 @@
</div> </div>
</div> </div>
<style>
.datetime-info {
font-family: monospace;
}
.datetime-info .date {
font-weight: 600;
color: #0f172a;
font-size: 0.875rem;
}
.datetime-info .time {
color: #64748b;
font-size: 0.75rem;
}
.location-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.location-info i {
color: #64748b;
font-size: 0.75rem;
}
.address-cell {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.8125rem;
color: #64748b;
}
.text-muted {
color: #9ca3af;
font-style: italic;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.filter-actions {
display: flex;
gap: 0.5rem;
}
/* Modal Styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #ffffff;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
}
.modal-close {
background: none;
border: none;
color: #64748b;
cursor: pointer;
font-size: 1.25rem;
padding: 0.5rem;
border-radius: 0.25rem;
transition: color 0.2s ease-in-out;
}
.modal-close:hover {
color: #0f172a;
}
.modal-body {
padding: 1.5rem;
}
.record-details {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
}
.record-details div {
margin-bottom: 0.5rem;
}
.record-details div:last-child {
margin-bottom: 0;
}
.warning-text {
color: #dc2626;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
margin-bottom: 0;
}
.modal-footer {
display: flex;
gap: 1rem;
justify-content: flex-end;
padding: 1.5rem;
border-top: 1px solid #e2e8f0;
}
@media (max-width: 768px) {
.records-table {
font-size: 0.75rem;
}
.records-table th,
.records-table td {
padding: 0.5rem 0.25rem;
}
.employee-cell {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.employee-avatar {
width: 24px;
height: 24px;
font-size: 0.625rem;
}
.action-buttons {
flex-direction: column;
gap: 0.25rem;
}
.action-btn {
padding: 0.25rem 0.5rem;
font-size: 0.625rem;
}
.filter-form {
grid-template-columns: 1fr;
}
.pagination-section {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.modal-content {
margin: 1rem;
width: calc(100% - 2rem);
}
.modal-footer {
flex-direction: column;
}
}
</style>
<script> <script>
// Filter toggle functionality // Toggle filters
function toggleFilters() { function toggleFilters() {
const filterBody = document.getElementById('filterBody'); const filterBody = document.getElementById('filterBody');
const filterIcon = document.getElementById('filterIcon'); const filterIcon = document.getElementById('filterIcon');
@@ -552,6 +436,58 @@ function toggleFilters() {
} }
} }
// Toggle export menu
function toggleExportMenu() {
const menu = document.getElementById('exportMenu');
menu.classList.toggle('show');
}
// Close export menu when clicking outside
document.addEventListener('click', function(e) {
const exportDropdown = document.querySelector('.export-dropdown');
if (exportDropdown && !exportDropdown.contains(e.target)) {
document.getElementById('exportMenu').classList.remove('show');
}
});
// Get current filters as URL parameters
function getCurrentFilters() {
const params = new URLSearchParams(window.location.search);
return {
employee_id: params.get('employee_id') || '',
location_name: params.get('location_name') || '',
start_date: params.get('start_date') || '',
end_date: params.get('end_date') || '',
import_batch: params.get('import_batch') || ''
};
}
// Export to CSV
function exportToCSV() {
const filters = getCurrentFilters();
const params = new URLSearchParams(filters);
params.set('format', 'csv');
window.location.href = `{{ url_for('export_time_attendance') }}?${params.toString()}`;
toggleExportMenu();
}
// Export to Excel
function exportToExcel() {
const filters = getCurrentFilters();
const params = new URLSearchParams(filters);
params.set('format', 'excel');
window.location.href = `{{ url_for('export_time_attendance') }}?${params.toString()}`;
toggleExportMenu();
}
// View record details
function viewRecord(recordId) {
// Implement view details functionality
alert(`View details for record ID: ${recordId}\nThis feature can be implemented to show a modal with full record details.`);
}
// Delete confirmation // Delete confirmation
function confirmDelete(recordId, employeeName, date) { function confirmDelete(recordId, employeeName, date) {
document.getElementById('deleteEmployeeName').textContent = employeeName; document.getElementById('deleteEmployeeName').textContent = employeeName;
@@ -564,13 +500,6 @@ function closeDeleteModal() {
document.getElementById('deleteModal').style.display = 'none'; document.getElementById('deleteModal').style.display = 'none';
} }
// Export functionality
function exportRecords() {
const params = new URLSearchParams(window.location.search);
params.set('export', 'csv');
window.location.href = `{{ url_for('time_attendance_records') }}?${params.toString()}`;
}
// Close modal when clicking outside // Close modal when clicking outside
document.getElementById('deleteModal').addEventListener('click', function(e) { document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) { if (e.target === this) {