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 werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
@@ -6541,7 +6541,7 @@ def time_attendance_dashboard():
return redirect(url_for('dashboard'))
@app.route('/time-attendance/import', methods=['GET', 'POST'])
@admin_required
@login_required
@log_database_operations('time_attendance_import')
def import_time_attendance():
"""Enhanced import with duplicate review"""
@@ -6678,7 +6678,7 @@ def import_time_attendance():
@app.route('/time-attendance/import/analyze-duplicates', methods=['POST'])
@admin_required
@login_required
def analyze_import_duplicates():
"""AJAX endpoint to analyze file for duplicates"""
try:
@@ -6744,7 +6744,7 @@ def analyze_import_duplicates():
@app.route('/time-attendance/import/cancel-pending')
@admin_required
@login_required
def cancel_pending_import():
"""Cancel pending import and cleanup temp file"""
try:
@@ -6766,7 +6766,7 @@ def cancel_pending_import():
@app.route('/time-attendance/import/validate', methods=['POST'])
@admin_required
@login_required
def validate_import_file():
"""AJAX endpoint to validate Excel file before import"""
try:
@@ -6967,6 +6967,322 @@ def download_import_template():
logger_handler.logger.error(f"Error generating template: {e}")
flash('Error generating template file.', 'error')
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')
@login_required
+107 -8
View File
@@ -111,6 +111,105 @@
@keyframes spin {
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>
{% endblock %}
@@ -277,14 +376,6 @@
Import Options
</h3>
<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">
<input type="checkbox" id="analyze_duplicates" name="analyze_duplicates" value="true">
<label for="analyze_duplicates" class="checkbox-label">
@@ -293,6 +384,14 @@
</label>
</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">
<input type="checkbox" id="validate_only" name="validate_only" value="true">
<label for="validate_only" class="checkbox-label">
+252 -323
View File
@@ -3,6 +3,94 @@
{% block extra_head %}
<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 %}
{% block page_title %}Time Attendance Records{% endblock %}
@@ -35,10 +123,40 @@
Import Data
</a>
{% endif %}
<button class="btn btn-secondary" onclick="exportRecords()">
<i class="fas fa-download"></i>
Export
</button>
<!-- Export Dropdown -->
<div class="export-dropdown">
<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>
@@ -99,117 +217,84 @@
value="{{ current_filters.end_date or '' }}">
</div>
<div class="form-group">
<label class="form-label">&nbsp;</label>
<div class="filter-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
Apply Filters
</button>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
<i class="fas fa-times"></i>
Clear
</a>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
Apply Filters
</button>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
<i class="fas fa-undo"></i>
Clear Filters
</a>
</div>
</form>
</div>
</div>
<!-- Records Table -->
<div class="records-section">
<div class="records-header">
<h2>
<i class="fas fa-table"></i>
Attendance Records
</h2>
<div class="records-meta">
{% if records.items %}
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>
<!-- Records Count Info -->
{% if records %}
<div class="records-info">
<div class="info-card">
<i class="fas fa-database"></i>
<span>Showing {{ records.items|length }} of {{ records.total }} records</span>
{% if current_filters.employee_id or current_filters.location_name or current_filters.start_date or current_filters.end_date %}
<span class="filter-badge">Filtered</span>
{% endif %}
</div>
{% if records.items %}
<div class="records-table-container">
</div>
{% endif %}
<!-- Records Table -->
{% if records and records.items %}
<div class="table-section">
<div class="table-container">
<table class="records-table">
<thead>
<tr>
<th>ID</th>
<th>Employee</th>
<th>Date & Time</th>
<th>Action</th>
<th>Date</th>
<th>Time</th>
<th>Location</th>
<th>Action</th>
<th>Platform</th>
<th>Address</th>
<th>Import Source</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for record in records.items %}
<tr>
<td>{{ record.id }}</td>
<td>
<div class="employee-cell">
<div class="employee-avatar">
{{ record.employee_name[0].upper() if record.employee_name else record.employee_id[0].upper() }}
</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 class="employee-info">
<strong>{{ record.employee_name }}</strong>
<small>ID: {{ record.employee_id }}</small>
</div>
</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>
<span class="action-badge {{ record.action_description.lower().replace(' ', '-') }}">
{% 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 %}
<span class="action-badge {% if 'Check In' in record.action_description %}check-in{% else %}check-out{% endif %}">
{{ record.action_description }}
</span>
</td>
<td class="location-cell">
<div class="location-info">
<i class="fas fa-map-marker-alt"></i>
{{ record.location_name }}
<td>{{ record.platform or 'N/A' }}</td>
<td>
<div class="import-info">
{{ record.import_source or 'Unknown' }}
<small>{{ record.import_date.strftime('%Y-%m-%d') if record.import_date else '' }}</small>
</div>
</td>
<td class="platform-cell">
{{ record.platform or 'Unknown' }}
</td>
<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">
<td>
<div class="record-actions">
<button class="action-btn btn-view" onclick="viewRecord({{ record.id }})" title="View Details">
<i class="fas fa-eye"></i>
View
</a>
</button>
{% if session.role == 'admin' %}
<button class="action-btn delete"
onclick="confirmDelete({{ record.id }}, '{{ record.employee_name }}', '{{ record.attendance_date }}')">
<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">
<i class="fas fa-trash"></i>
Delete
</button>
{% endif %}
</div>
@@ -219,7 +304,7 @@
</tbody>
</table>
</div>
<!-- Pagination -->
{% if records.pages > 1 %}
<div class="pagination-section">
@@ -268,37 +353,40 @@
</div>
</div>
{% endif %}
</div>
{% else %}
<!-- Empty State -->
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3>No Records Found</h3>
<p>
{% 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.
Try adjusting your filters or clearing them to see all records.
{% else %}
No time attendance records have been imported yet.
Import your first Excel file to get started.
{% endif %}
</p>
{% else %}
<!-- Empty State -->
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h3>No Records Found</h3>
<p>
{% 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.
Try adjusting your filters or clearing them to see all records.
{% else %}
No time attendance records have been imported yet.
Import your first Excel file to get started.
{% endif %}
</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 %}
<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
</a>
{% elif session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary">
{% endif %}
{% if session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-secondary">
<i class="fas fa-upload"></i>
Import Records
Import Data
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Delete Confirmation Modal -->
@@ -306,7 +394,7 @@
<div class="modal-content">
<div class="modal-header">
<h3>
<i class="fas fa-exclamation-triangle text-danger"></i>
<i class="fas fa-exclamation-triangle"></i>
Confirm Deletion
</h3>
<button class="modal-close" onclick="closeDeleteModal()">
@@ -315,14 +403,9 @@
</div>
<div class="modal-body">
<p>Are you sure you want to delete this attendance record?</p>
<div class="record-details">
<div><strong>Employee:</strong> <span id="deleteEmployeeName"></span></div>
<div><strong>Date:</strong> <span id="deleteDate"></span></div>
</div>
<p class="warning-text">
<i class="fas fa-exclamation-triangle"></i>
This action cannot be undone.
</p>
<p><strong>Employee:</strong> <span id="deleteEmployeeName"></span></p>
<p><strong>Date:</strong> <span id="deleteDate"></span></p>
<p class="text-danger">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>
@@ -336,207 +419,8 @@
</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>
// Filter toggle functionality
// Toggle filters
function toggleFilters() {
const filterBody = document.getElementById('filterBody');
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
function confirmDelete(recordId, employeeName, date) {
document.getElementById('deleteEmployeeName').textContent = employeeName;
@@ -564,13 +500,6 @@ function closeDeleteModal() {
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
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {