04/08 Time Attendance: added checkbox to allow export unlimited date range

This commit is contained in:
2026-04-09 11:19:08 -04:00
parent bbf2716368
commit 07c540c7f9
3 changed files with 47 additions and 19 deletions
+10 -10
View File
@@ -1146,6 +1146,8 @@ def export_time_attendance():
end_date = request.args.get('end_date') end_date = request.args.get('end_date')
import_batch = request.args.get('import_batch') import_batch = request.args.get('import_batch')
project_filter = request.args.get('project_id') project_filter = request.args.get('project_id')
# unlimited=true skips the 14-day cap; default is capped (2-week) mode
unlimited = request.args.get('unlimited', 'false').lower() == 'true'
# Build query with same filters as the view # Build query with same filters as the view
query = TimeAttendance.query query = TimeAttendance.query
@@ -1224,7 +1226,7 @@ def export_time_attendance():
# Log export # Log export
logger_handler.logger.info( logger_handler.logger.info(
f"User {session['username']} exported {len(records)} time attendance records " f"User {session['username']} exported {len(records)} time attendance records "
f"in {export_format.upper()} format" f"in {export_format.upper()} format (unlimited={unlimited})"
) )
# Format dates for filename (MMDDYYYY format) # Format dates for filename (MMDDYYYY format)
@@ -1263,7 +1265,7 @@ def export_time_attendance():
filter_str = "_".join(filter_desc) if filter_desc else "all" filter_str = "_".join(filter_desc) if filter_desc else "all"
return export_time_attendance_excel(records, project_name_for_filename, date_range_str, filter_str, start_date, end_date) return export_time_attendance_excel(records, project_name_for_filename, date_range_str, filter_str, start_date, end_date, unlimited=unlimited)
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error exporting time attendance records: {e}") logger_handler.logger.error(f"Error exporting time attendance records: {e}")
@@ -1291,10 +1293,12 @@ def export_time_attendance_by_building():
end_date = request.args.get('end_date') end_date = request.args.get('end_date')
import_batch = request.args.get('import_batch') import_batch = request.args.get('import_batch')
project_filter = request.args.get('project_id') project_filter = request.args.get('project_id')
# unlimited=true skips the 14-day cap; default is capped (2-week) mode
unlimited = request.args.get('unlimited', 'false').lower() == 'true'
# Build query with same filters as the view # Build query with same filters as the view
query = TimeAttendance.query query = TimeAttendance.query
# Apply filters — employee_id supports comma-separated multi-employee values # Apply filters — employee_id supports comma-separated multi-employee values
if employee_filter: if employee_filter:
employee_ids_export = [e.strip() for e in employee_filter.split(',') if e.strip()] employee_ids_export = [e.strip() for e in employee_filter.split(',') if e.strip()]
@@ -1315,8 +1319,6 @@ def export_time_attendance_by_building():
if location_filter: if location_filter:
query = query.filter(TimeAttendance.location_name == location_filter) query = query.filter(TimeAttendance.location_name == location_filter)
if start_date:
try: try:
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
query = query.filter(TimeAttendance.attendance_date >= start_date_obj) query = query.filter(TimeAttendance.attendance_date >= start_date_obj)
@@ -1369,7 +1371,7 @@ def export_time_attendance_by_building():
# Log export # Log export
logger_handler.logger.info( logger_handler.logger.info(
f"User {session['username']} exported {len(records)} time attendance records " f"User {session['username']} exported {len(records)} time attendance records "
f"by building in Excel format" f"by building in Excel format (unlimited={unlimited})"
) )
# Format dates for filename (MMDDYYYY format) # Format dates for filename (MMDDYYYY format)
@@ -1398,7 +1400,7 @@ def export_time_attendance_by_building():
elif date_to_formatted: elif date_to_formatted:
date_range_str = f"to_{date_to_formatted}" date_range_str = f"to_{date_to_formatted}"
return export_time_attendance_by_building_excel(records, project_name_for_filename, date_range_str, start_date, end_date) return export_time_attendance_by_building_excel(records, project_name_for_filename, date_range_str, start_date, end_date, unlimited=unlimited)
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error exporting time attendance records by building: {e}") logger_handler.logger.error(f"Error exporting time attendance records by building: {e}")
@@ -1690,6 +1692,4 @@ def api_time_attendance_by_location(location_name):
'error': 'Failed to retrieve time attendance records' 'error': 'Failed to retrieve time attendance records'
}), 500 }), 500
# Jinja2 filters for better template functionality # Jinja2 filters for better template functionality
+9 -9
View File
@@ -102,14 +102,14 @@ def _qtr(decimal_hours: float) -> float:
# Private export helpers — shared by both export functions below. # Private export helpers — shared by both export functions below.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _resolve_date_range(start_date_filter, end_date_filter, records, export_label='TA Excel export'): def _resolve_date_range(start_date_filter, end_date_filter, records, export_label='TA Excel export', unlimited=False):
""" """
Resolve and validate the export date range. Resolve and validate the export date range.
Returns (start_date, end_date, filtered_records) where: Returns (start_date, end_date, filtered_records) where:
- start_date / end_date are date objects - start_date / end_date are date objects
- filtered_records is the input list capped to end_date + 1 day (overnight buffer) - filtered_records is the input list capped to end_date + 1 day (overnight buffer)
- end_date is capped to a maximum 14-day window - end_date is capped to a maximum 14-day window unless unlimited=True
Returns None when there are no records and no date filters. Returns None when there are no records and no date filters.
""" """
MAX_EXPORT_DAYS = 14 MAX_EXPORT_DAYS = 14
@@ -129,7 +129,7 @@ def _resolve_date_range(start_date_filter, end_date_filter, records, export_labe
else: else:
return None return None
if (end_date - start_date).days >= MAX_EXPORT_DAYS: if not unlimited and (end_date - start_date).days >= MAX_EXPORT_DAYS:
capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1) capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1)
logger_handler.logger.info( logger_handler.logger.info(
f"{export_label}: date range [{start_date} \u2013 {end_date}] exceeds " f"{export_label}: date range [{start_date} \u2013 {end_date}] exceeds "
@@ -272,7 +272,7 @@ def _make_export_styles():
} }
def export_time_attendance_excel(records, project_name_for_filename, date_range_str, filter_str, start_date_filter=None, end_date_filter=None): def export_time_attendance_excel(records, project_name_for_filename, date_range_str, filter_str, start_date_filter=None, end_date_filter=None, unlimited=False):
"""Generate Excel export with template format matching the provided template""" """Generate Excel export with template format matching the provided template"""
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
@@ -284,8 +284,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
ws = wb.active ws = wb.active
ws.title = "Sheet0" ws.title = "Sheet0"
# Resolve date range and cap to 14-day window # Resolve date range; skip 14-day cap when unlimited=True
result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA Excel export') result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA Excel export', unlimited=unlimited)
if result is None: if result is None:
return None return None
start_date, end_date, records = result start_date, end_date, records = result
@@ -1345,7 +1345,7 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
) )
def export_time_attendance_by_building_excel(records, project_name_for_filename, date_range_str, start_date_filter=None, end_date_filter=None): def export_time_attendance_by_building_excel(records, project_name_for_filename, date_range_str, start_date_filter=None, end_date_filter=None, unlimited=False):
"""Generate Excel export grouped by building/location with template format""" """Generate Excel export grouped by building/location with template format"""
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
@@ -1357,8 +1357,8 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename,
ws = wb.active ws = wb.active
ws.title = "Sheet0" ws.title = "Sheet0"
# Resolve date range and cap to 14-day window # Resolve date range; skip 14-day cap when unlimited=True
result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA by-building Excel export') result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA by-building Excel export', unlimited=unlimited)
if result is None: if result is None:
return None return None
start_date, end_date, records = result start_date, end_date, records = result
+28
View File
@@ -4,6 +4,26 @@
{% 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> <style>
/* ── Unlimited export toggle ── */
.export-unlimited-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--gray-700);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.export-unlimited-toggle input[type="checkbox"] {
width: 1rem;
height: 1rem;
accent-color: var(--primary-color);
cursor: pointer;
flex-shrink: 0;
}
/* ── Autocomplete widget styles (mirrors attendance.css — not loaded on this page) ── */ /* ── Autocomplete widget styles (mirrors attendance.css — not loaded on this page) ── */
.autocomplete-container-filter { position: relative; } .autocomplete-container-filter { position: relative; }
@@ -267,6 +287,10 @@
Import Data Import Data
</a> </a>
{% endif %} {% endif %}
<label class="export-unlimited-toggle" title="When checked, exports all dates in the selected range instead of capping at 2 weeks">
<input type="checkbox" id="export_unlimited">
<span>Unlimited</span>
</label>
<button class="btn btn-secondary" onclick="exportRecords('excel')"> <button class="btn btn-secondary" onclick="exportRecords('excel')">
<i class="fas fa-file-excel"></i> <i class="fas fa-file-excel"></i>
Export to Excel Export to Excel
@@ -627,6 +651,7 @@ function exportRecords(format) {
const projectId = document.querySelector('select[name="project_id"]')?.value || ''; const projectId = document.querySelector('select[name="project_id"]')?.value || '';
const startDate = document.querySelector('input[name="start_date"]')?.value || ''; const startDate = document.querySelector('input[name="start_date"]')?.value || '';
const endDate = document.querySelector('input[name="end_date"]')?.value || ''; const endDate = document.querySelector('input[name="end_date"]')?.value || '';
const unlimited = document.getElementById('export_unlimited')?.checked || false;
// Build query parameters // Build query parameters
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -636,6 +661,7 @@ function exportRecords(format) {
if (startDate) params.append('start_date', startDate); if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate); if (endDate) params.append('end_date', endDate);
params.append('format', format); params.append('format', format);
if (unlimited) params.append('unlimited', 'true');
// Redirect to export endpoint // Redirect to export endpoint
window.location.href = `/time-attendance/export?${params.toString()}`; window.location.href = `/time-attendance/export?${params.toString()}`;
@@ -648,6 +674,7 @@ function exportByBuilding() {
const projectId = document.querySelector('select[name="project_id"]')?.value || ''; const projectId = document.querySelector('select[name="project_id"]')?.value || '';
const startDate = document.querySelector('input[name="start_date"]')?.value || ''; const startDate = document.querySelector('input[name="start_date"]')?.value || '';
const endDate = document.querySelector('input[name="end_date"]')?.value || ''; const endDate = document.querySelector('input[name="end_date"]')?.value || '';
const unlimited = document.getElementById('export_unlimited')?.checked || false;
// Build query parameters // Build query parameters
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -656,6 +683,7 @@ function exportByBuilding() {
if (projectId) params.append('project_id', projectId); if (projectId) params.append('project_id', projectId);
if (startDate) params.append('start_date', startDate); if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate); if (endDate) params.append('end_date', endDate);
if (unlimited) params.append('unlimited', 'true');
// Redirect to export by building endpoint // Redirect to export by building endpoint
window.location.href = `/time-attendance/export-by-building?${params.toString()}`; window.location.href = `/time-attendance/export-by-building?${params.toString()}`;