Updated TA export function
This commit is contained in:
@@ -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')
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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()">
|
|
||||||
|
<!-- Export Dropdown -->
|
||||||
|
<div class="export-dropdown">
|
||||||
|
<button class="btn btn-secondary" onclick="toggleExportMenu()">
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
Export
|
Export
|
||||||
|
<i class="fas fa-chevron-down" style="margin-left: 0.5rem; font-size: 0.75rem;"></i>
|
||||||
</button>
|
</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"> </label>
|
|
||||||
<div class="filter-actions">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
Apply Filters
|
Apply Filters
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
|
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-outline">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-undo"></i>
|
||||||
Clear
|
Clear Filters
|
||||||
</a>
|
</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 %}
|
|
||||||
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 %}
|
{% 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-avatar">
|
|
||||||
{{ record.employee_name[0].upper() if record.employee_name else record.employee_id[0].upper() }}
|
|
||||||
</div>
|
|
||||||
<div class="employee-info">
|
<div class="employee-info">
|
||||||
<div class="employee-name">{{ record.employee_name }}</div>
|
<strong>{{ record.employee_name }}</strong>
|
||||||
<div class="employee-id">ID: {{ record.employee_id }}</div>
|
<small>ID: {{ record.employee_id }}</small>
|
||||||
</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,6 +353,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
@@ -285,20 +371,22 @@
|
|||||||
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user