diff --git a/app.py b/app.py index 7155c02..f15a5e1 100644 --- a/app.py +++ b/app.py @@ -5404,6 +5404,89 @@ def save_manual_attendance(): return redirect(url_for('add_manual_attendance')) +@app.route('/api/time-attendance/locations') +@login_required +def time_attendance_locations_api(): + """Return distinct location_name values from time_attendance, optionally filtered by project_id. + Used by the time attendance records page to dynamically scope the location dropdown.""" + try: + project_id = request.args.get('project_id', '').strip() + + if project_id: + try: + project_id_int = int(project_id) + except (ValueError, TypeError): + return jsonify({'success': False, 'error': 'Invalid project_id'}), 400 + + result = db.session.execute(text(""" + SELECT DISTINCT location_name + FROM time_attendance + WHERE project_id = :project_id + AND location_name IS NOT NULL + ORDER BY location_name + """), {'project_id': project_id_int}) + else: + result = db.session.execute(text(""" + SELECT DISTINCT location_name + FROM time_attendance + WHERE location_name IS NOT NULL + ORDER BY location_name + """)) + + locations = [row[0] for row in result.fetchall()] + logger_handler.logger.info( + f"User {session.get('username', 'unknown')} fetched time attendance locations" + + (f" for project_id={project_id}" if project_id else " (all projects)") + ) + return jsonify({'success': True, 'locations': locations}) + + except Exception as e: + logger_handler.logger.error(f"Error in time_attendance_locations_api: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/attendance/locations') +@login_required +def attendance_locations_api(): + """Return distinct location_name values from attendance_data, optionally filtered by project_id. + Used by the attendance report page to dynamically scope the location dropdown when a project is selected.""" + try: + project_id = request.args.get('project_id', '').strip() + + if project_id: + try: + project_id_int = int(project_id) + except (ValueError, TypeError): + return jsonify({'success': False, 'error': 'Invalid project_id'}), 400 + + result = db.session.execute(text(""" + SELECT DISTINCT ad.location_name + FROM attendance_data ad + INNER JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE qc.project_id = :project_id + AND ad.location_name IS NOT NULL + ORDER BY ad.location_name + """), {'project_id': project_id_int}) + else: + result = db.session.execute(text(""" + SELECT DISTINCT location_name + FROM attendance_data + WHERE location_name IS NOT NULL + ORDER BY location_name + """)) + + locations = [row[0] for row in result.fetchall()] + logger_handler.logger.info( + f"User {session.get('username', 'unknown')} fetched attendance locations" + + (f" for project_id={project_id}" if project_id else " (all projects)") + ) + return jsonify({'success': True, 'locations': locations}) + + except Exception as e: + logger_handler.logger.error(f"Error in attendance_locations_api: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/search_employees') @login_required def search_employees_api(): @@ -9230,26 +9313,27 @@ def export_time_attendance(): from models.time_attendance import TimeAttendance query = TimeAttendance.query - # Apply filters + # Apply filters — employee_id supports comma-separated multi-employee values if employee_filter: - # Include all SP/PW/PT work-type variants of the base employee ID so that - # cross-type pairing (e.g. regular IN + SP OUT) works correctly in the export. + employee_ids_export = [e.strip() for e in employee_filter.split(',') if e.strip()] from working_hours_calculator import parse_employee_id_for_work_type as _parse_wt - _base_emp_id, _ = _parse_wt(str(employee_filter)) - _emp_id_variants = [ - _base_emp_id, - f"{_base_emp_id} SP", f"{_base_emp_id}SP", - f"SP {_base_emp_id}", f"SP{_base_emp_id}", - f"{_base_emp_id} PW", f"{_base_emp_id}PW", - f"PW {_base_emp_id}", f"PW{_base_emp_id}", - f"{_base_emp_id} PT", f"{_base_emp_id}PT", - f"PT {_base_emp_id}", f"PT{_base_emp_id}", - ] - query = query.filter(TimeAttendance.employee_id.in_(_emp_id_variants)) - + all_variants = [] + for eid in employee_ids_export: + _base_emp_id, _ = _parse_wt(str(eid)) + all_variants += [ + _base_emp_id, + f"{_base_emp_id} SP", f"{_base_emp_id}SP", + f"SP {_base_emp_id}", f"SP{_base_emp_id}", + f"{_base_emp_id} PW", f"{_base_emp_id}PW", + f"PW {_base_emp_id}", f"PW{_base_emp_id}", + f"{_base_emp_id} PT", f"{_base_emp_id}PT", + f"PT {_base_emp_id}", f"PT{_base_emp_id}", + ] + query = query.filter(TimeAttendance.employee_id.in_(all_variants)) + 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() @@ -9257,7 +9341,7 @@ def export_time_attendance(): 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() @@ -9271,23 +9355,23 @@ def export_time_attendance(): 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) - + if project_filter: query = query.filter(TimeAttendance.project_id == project_filter) - + # 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')) - + # Get project name if project filter exists project_name_for_filename = '' if project_filter: @@ -9300,7 +9384,7 @@ def export_time_attendance(): project_name_for_filename = f"{project_name_safe}_" except Exception as e: print(f"⚠️ Error getting project name for filename: {e}") - + # Log export logger_handler.logger.info( f"User {session['username']} exported {len(records)} time attendance records " @@ -10611,25 +10695,27 @@ def export_time_attendance_by_building(): from models.time_attendance import TimeAttendance query = TimeAttendance.query - # Apply filters + # Apply filters — employee_id supports comma-separated multi-employee values if employee_filter: - # Include all SP/PW/PT work-type variants of the base employee ID. + employee_ids_export = [e.strip() for e in employee_filter.split(',') if e.strip()] from working_hours_calculator import parse_employee_id_for_work_type as _parse_wt - _base_emp_id, _ = _parse_wt(str(employee_filter)) - _emp_id_variants = [ - _base_emp_id, - f"{_base_emp_id} SP", f"{_base_emp_id}SP", - f"SP {_base_emp_id}", f"SP{_base_emp_id}", - f"{_base_emp_id} PW", f"{_base_emp_id}PW", - f"PW {_base_emp_id}", f"PW{_base_emp_id}", - f"{_base_emp_id} PT", f"{_base_emp_id}PT", - f"PT {_base_emp_id}", f"PT{_base_emp_id}", - ] - query = query.filter(TimeAttendance.employee_id.in_(_emp_id_variants)) - + all_variants = [] + for eid in employee_ids_export: + _base_emp_id, _ = _parse_wt(str(eid)) + all_variants += [ + _base_emp_id, + f"{_base_emp_id} SP", f"{_base_emp_id}SP", + f"SP {_base_emp_id}", f"SP{_base_emp_id}", + f"{_base_emp_id} PW", f"{_base_emp_id}PW", + f"PW {_base_emp_id}", f"PW{_base_emp_id}", + f"{_base_emp_id} PT", f"{_base_emp_id}PT", + f"PT {_base_emp_id}", f"PT{_base_emp_id}", + ] + query = query.filter(TimeAttendance.employee_id.in_(all_variants)) + 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() @@ -10637,7 +10723,7 @@ def export_time_attendance_by_building(): 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() @@ -10650,24 +10736,24 @@ def export_time_attendance_by_building(): 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) - + if project_filter: query = query.filter(TimeAttendance.project_id == project_filter) - + # Order by location, date, and time records = query.order_by( TimeAttendance.location_name, 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')) - + # Get project name if project filter exists project_name_for_filename = '' if project_filter: @@ -10680,7 +10766,7 @@ def export_time_attendance_by_building(): project_name_for_filename = f"{project_name_safe}_" except Exception as e: print(f"⚠️ Error getting project name for filename: {e}") - + # Log export logger_handler.logger.info( f"User {session['username']} exported {len(records)} time attendance records " @@ -11471,20 +11557,61 @@ def time_attendance_records(): """Display time attendance records with filtering options""" try: # Get filter parameters - employee_filter = request.args.get('employee_id') + 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') project_filter = request.args.get('project_id') page = request.args.get('page', 1, type=int) per_page = 50 # Records per page - + + # Build list of selected employee IDs (comma-separated multi-employee support) + employee_ids = [e.strip() for e in employee_filter.split(',') if e.strip()] if employee_filter else [] + + # Build display names for each selected employee + import re as _re + employee_display_names = [] + for eid in employee_ids: + try: + numeric_only = _re.search(r'\d+', str(eid)) + if numeric_only: + emp = Employee.query.filter_by(id=int(numeric_only.group(0))).first() + if emp: + employee_display_names.append({'id': eid, 'name': f"{emp.lastName}, {emp.firstName}"}) + else: + employee_display_names.append({'id': eid, 'name': f"ID: {eid}"}) + else: + employee_display_names.append({'id': eid, 'name': eid}) + except (ValueError, TypeError): + employee_display_names.append({'id': eid, 'name': eid}) + + employee_display_name = ', '.join([e['name'] for e in employee_display_names]) + # Build query query = TimeAttendance.query - + # Apply filters - if employee_filter: - query = query.filter(TimeAttendance.employee_id == employee_filter) + if employee_ids: + # Expand each base ID to include all SP/PW/PT work-type variants so that + # cross-type pairs are included in results and exports. + from working_hours_calculator import parse_employee_id_for_work_type as _parse_wt + all_variants = [] + for eid in employee_ids: + _base_emp_id, _ = _parse_wt(str(eid)) + all_variants += [ + _base_emp_id, + f"{_base_emp_id} SP", f"{_base_emp_id}SP", + f"SP {_base_emp_id}", f"SP{_base_emp_id}", + f"{_base_emp_id} PW", f"{_base_emp_id}PW", + f"PW {_base_emp_id}", f"PW{_base_emp_id}", + f"{_base_emp_id} PT", f"{_base_emp_id}PT", + f"PT {_base_emp_id}", f"PT{_base_emp_id}", + ] + query = query.filter(TimeAttendance.employee_id.in_(all_variants)) + logger_handler.logger.info( + f"Time attendance records filtered by employee IDs: {employee_ids} " + f"by user {session.get('username', 'unknown')}" + ) if location_filter: query = query.filter(TimeAttendance.location_name == location_filter) @@ -11563,23 +11690,6 @@ def time_attendance_records(): unique_locations = TimeAttendance.get_unique_locations() projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() - # Resolve display name for the employee filter input - employee_display_name = '' - if employee_filter: - try: - import re as _re - numeric_only = _re.search(r'\d+', str(employee_filter)) - if numeric_only: - emp = Employee.query.filter_by(id=int(numeric_only.group(0))).first() - if emp: - employee_display_name = f"{emp.lastName}, {emp.firstName}" - else: - employee_display_name = f"ID: {employee_filter}" - else: - employee_display_name = f"ID: {employee_filter}" - except (ValueError, TypeError): - employee_display_name = employee_filter - return render_template( 'time_attendance_records.html', records=records, @@ -11587,7 +11697,9 @@ def time_attendance_records(): unique_locations=unique_locations, projects=projects, employee_display_name=employee_display_name, - employee_filter=employee_filter or '' + employee_display_names=employee_display_names, + employee_filter=employee_filter, + employee_ids=employee_ids ) except Exception as e: diff --git a/templates/attendance_report.html b/templates/attendance_report.html index b93e2bd..e1f6221 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -1003,4 +1003,83 @@ document.addEventListener('DOMContentLoaded', function() { }; }()); + {% endblock %} \ No newline at end of file diff --git a/templates/time_attendance_records.html b/templates/time_attendance_records.html index d360a1f..d33006a 100644 --- a/templates/time_attendance_records.html +++ b/templates/time_attendance_records.html @@ -62,6 +62,90 @@ .filter-input { padding-right: 35px !important; } +/* ── Multi-Employee Chip Filter ── */ +.employee-chips-wrapper { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + min-height: 38px; + padding: 4px 36px 4px 8px; + border: 2px solid #e2e8f0; + border-radius: 0.5rem; + background: #fff; + cursor: text; + position: relative; + box-sizing: border-box; + width: 100%; + transition: border-color 0.2s, box-shadow 0.2s; +} +.employee-chips-wrapper:focus-within { + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37,99,235,.1); +} +.employee-chip { + display: inline-flex; + align-items: center; + gap: 5px; + background: #eef2ff; + color: #4338ca; + border: 1px solid #c7d2fe; + border-radius: 4px; + padding: 2px 6px 2px 8px; + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; +} +.employee-chip-remove { + background: none; + border: none; + cursor: pointer; + color: #6366f1; + padding: 0; + line-height: 1; + font-size: 0.7rem; + display: flex; + align-items: center; + border-radius: 2px; + transition: color 0.15s; + flex-shrink: 0; +} +.employee-chip-remove:hover { color: #dc2626; } +.employee-chip-input { + border: none; + outline: none; + font-size: 0.875rem; + background: transparent; + flex: 1; + min-width: 120px; + padding: 2px 0; + color: #1f2937; +} +.employee-chip-input::placeholder { color: #9ca3af; } +.employee-chips-clear-all { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + background: #dc3545; + color: white; + border: none; + border-radius: 50%; + width: 22px; + height: 22px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; + padding: 0; +} +.employee-chips-clear-all:hover { background: #c82333; } +.employee-chips-clear-all i { font-size: 0.7rem; } + /* ── Mirror attendance.css filter layout on this page ── */ .filters-section { margin-bottom: 2rem; } @@ -260,30 +344,33 @@
-
@@ -596,72 +683,158 @@ document.addEventListener('keydown', function(event) { // Filters are always visible — no toggle needed -// ── Employee autocomplete ────────────────────────────────────────────────── +// ── Multi-Employee Chip Filter Autocomplete ─────────────────────────────── (function () { - const searchInput = document.getElementById('ta_employee_search'); - const hiddenInput = document.getElementById('employee_id'); - const dropdown = document.getElementById('ta_autocomplete_results'); - let debounceTimer; + const chipInput = document.getElementById('ta_employee_chip_input'); + const employeeHidden = document.getElementById('employee_id'); + const autocomplete = document.getElementById('ta_autocomplete_results'); + const chipsWrapper = document.getElementById('taEmployeeChipsWrapper'); + let searchTimeout; - if (!searchInput) return; + // State: map of id -> displayName for currently selected employees + const selected = {}; + {% for emp in employee_display_names %} + selected['{{ emp.id }}'] = '{{ emp.name | e }}'; + {% endfor %} - searchInput.addEventListener('input', function () { - const term = this.value.trim(); - - // Keep hidden field in sync when typing a plain numeric ID so the user - // can submit without selecting from the dropdown (handles IDs not in Employee table). - if (/^\d+$/.test(term)) { - hiddenInput.value = term; - } else { - hiddenInput.value = ''; + function syncHiddenField() { + employeeHidden.value = Object.keys(selected).join(','); + const hasChips = Object.keys(selected).length > 0; + let btn = document.getElementById('taClearAllEmployees'); + if (hasChips && !btn) { + btn = document.createElement('button'); + btn.type = 'button'; + btn.id = 'taClearAllEmployees'; + btn.className = 'employee-chips-clear-all'; + btn.title = 'Clear all'; + btn.innerHTML = ''; + btn.addEventListener('click', clearAll); + chipsWrapper.appendChild(btn); + } else if (!hasChips && btn) { + btn.remove(); } + } - if (term.length === 0) { - hiddenInput.value = ''; - dropdown.classList.remove('show'); - return; + function addChip(id, name) { + id = String(id); + if (selected[id]) return; + selected[id] = name; + + const chip = document.createElement('span'); + chip.className = 'employee-chip'; + chip.dataset.id = id; + + const nameSpan = document.createElement('span'); + nameSpan.title = name; + nameSpan.textContent = name; + + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'employee-chip-remove'; + removeBtn.title = 'Remove'; + removeBtn.innerHTML = ''; + removeBtn.addEventListener('click', function () { + delete selected[chip.dataset.id]; + chip.remove(); + syncHiddenField(); + updatePlaceholder(); + }); + + chip.appendChild(nameSpan); + chip.appendChild(removeBtn); + chipsWrapper.insertBefore(chip, chipInput); + syncHiddenField(); + updatePlaceholder(); + } + + function updatePlaceholder() { + chipInput.placeholder = Object.keys(selected).length > 0 + ? 'Add more...' + : 'Search by ID or Name...'; + } + + function clearAll() { + Object.keys(selected).forEach(function(k) { delete selected[k]; }); + chipsWrapper.querySelectorAll('.employee-chip').forEach(function(c) { c.remove(); }); + syncHiddenField(); + updatePlaceholder(); + } + + // Wire remove buttons for server-rendered chips on page load + chipsWrapper.querySelectorAll('.employee-chip-remove').forEach(function (btn) { + btn.addEventListener('click', function () { + const chip = btn.closest('.employee-chip'); + delete selected[chip.dataset.id]; + chip.remove(); + syncHiddenField(); + updatePlaceholder(); + }); + }); + + const clearAllBtn = document.getElementById('taClearAllEmployees'); + if (clearAllBtn) { + clearAllBtn.addEventListener('click', clearAll); + } + + chipsWrapper.addEventListener('click', function (e) { + if (!e.target.closest('.employee-chip') && !e.target.closest('.employee-chips-clear-all')) { + chipInput.focus(); } + }); - if (term.length < 2) { - dropdown.classList.remove('show'); - return; - } + chipInput.addEventListener('input', function () { + const q = this.value.trim(); + if (q.length === 0) { autocomplete.classList.remove('show'); return; } + if (q.length < 2) { autocomplete.classList.remove('show'); return; } - clearTimeout(debounceTimer); - debounceTimer = setTimeout(function () { - fetch('/api/search_employees?q=' + encodeURIComponent(term)) + clearTimeout(searchTimeout); + searchTimeout = setTimeout(function () { + fetch('/api/search_employees?q=' + encodeURIComponent(q)) .then(function (r) { return r.json(); }) .then(function (data) { renderDropdown(data.employees || []); }) .catch(function (err) { console.error('Employee search error:', err); }); }, 300); }); - document.addEventListener('click', function (e) { - if (!e.target.closest('.autocomplete-container-filter')) { - dropdown.classList.remove('show'); + chipInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + const q = chipInput.value.trim(); + if (/^\d+$/.test(q) && !selected[q]) { + addChip(q, 'ID: ' + q); + chipInput.value = ''; + autocomplete.classList.remove('show'); + } + } + if (e.key === 'Backspace' && chipInput.value === '') { + const chips = chipsWrapper.querySelectorAll('.employee-chip'); + if (chips.length > 0) { + const last = chips[chips.length - 1]; + delete selected[last.dataset.id]; + last.remove(); + syncHiddenField(); + updatePlaceholder(); + } } }); function renderDropdown(employees) { - // Use createElement + addEventListener — safe with apostrophes in names - dropdown.innerHTML = ''; + autocomplete.innerHTML = ''; + const unselected = employees.filter(function(emp) { return !selected[String(emp.id)]; }); - if (employees.length === 0) { + if (unselected.length === 0) { const noResult = document.createElement('div'); noResult.className = 'autocomplete-item-filter'; noResult.style.cssText = 'cursor:default;color:#999;'; - noResult.textContent = 'No employees found'; - dropdown.appendChild(noResult); + noResult.textContent = employees.length === 0 ? 'No employees found' : 'All matching employees already selected'; + autocomplete.appendChild(noResult); } else { - employees.forEach(function (emp) { + unselected.forEach(function (emp) { const isUnregistered = (emp.lastName === '(no record)'); - const displayName = isUnregistered - ? 'ID: ' + emp.id - : emp.lastName + ', ' + emp.firstName; + const displayName = isUnregistered ? 'ID: ' + emp.id : emp.lastName + ', ' + emp.firstName; const item = document.createElement('div'); item.className = 'autocomplete-item-filter'; - const info = document.createElement('div'); info.className = 'employee-info-filter'; @@ -685,47 +858,113 @@ document.addEventListener('keydown', function(event) { info.appendChild(idSpan); item.appendChild(info); - // Closure captures values safely regardless of special characters item.addEventListener('click', (function (id, name) { - return function () { taSelectEmployee(id, name); }; + return function () { + addChip(id, name); + chipInput.value = ''; + autocomplete.classList.remove('show'); + }; }(String(emp.id), displayName))); - dropdown.appendChild(item); + autocomplete.appendChild(item); }); } positionDropdown(); - dropdown.classList.add('show'); + autocomplete.classList.add('show'); } function positionDropdown() { - const rect = searchInput.getBoundingClientRect(); - dropdown.style.top = (rect.bottom + window.scrollY) + 'px'; - dropdown.style.left = rect.left + 'px'; - dropdown.style.width = rect.width + 'px'; + const wrapperRect = chipsWrapper.getBoundingClientRect(); + autocomplete.style.top = (wrapperRect.bottom + window.scrollY) + 'px'; + autocomplete.style.left = wrapperRect.left + 'px'; + autocomplete.style.width = wrapperRect.width + 'px'; } - window.taSelectEmployee = function (id, displayName) { - hiddenInput.value = id; - searchInput.value = displayName; - dropdown.classList.remove('show'); - }; - - window.taClearEmployee = function () { - searchInput.value = ''; - hiddenInput.value = ''; - searchInput.closest('form').submit(); - }; - - window.taClearAllFilters = function () { - window.location.href = '{{ url_for("time_attendance_records") }}'; - }; + document.addEventListener('click', function (e) { + if (!e.target.closest('.autocomplete-container-filter')) { + autocomplete.classList.remove('show'); + } + }); window.addEventListener('scroll', function () { - if (dropdown.classList.contains('show')) positionDropdown(); + if (autocomplete.classList.contains('show')) positionDropdown(); }, true); window.addEventListener('resize', function () { - if (dropdown.classList.contains('show')) positionDropdown(); + if (autocomplete.classList.contains('show')) positionDropdown(); + }); + + // Legacy globals + window.taSelectEmployee = function (id, name) { + addChip(String(id), name); + chipInput.value = ''; + autocomplete.classList.remove('show'); + }; + window.taClearEmployee = function () { + clearAll(); + chipsWrapper.closest('form').submit(); + }; + window.taClearAllFilters = function () { + window.location.href = '{{ url_for("time_attendance_records") }}'; + }; +}()); + +// ── Dynamic Location Dropdown (scoped by selected project) ──────────────── +(function () { + const projectSelect = document.getElementById('project_id'); + const locationSelect = document.getElementById('location_name'); + + if (!projectSelect || !locationSelect) return; + + // Capture full server-rendered location list on page load for restoration + const allLocationOptions = Array.from(locationSelect.options).map(function (opt) { + return { value: opt.value, text: opt.text }; + }); + const initialLocationValue = locationSelect.value; + + function rebuildLocationDropdown(locations, preserveValue) { + while (locationSelect.options.length > 1) { locationSelect.remove(1); } + locations.forEach(function (locName) { + const opt = document.createElement('option'); + opt.value = locName; + opt.textContent = locName; + if (locName === preserveValue) opt.selected = true; + locationSelect.appendChild(opt); + }); + } + + function loadLocationsForProject(projectId) { + fetch('/api/time-attendance/locations?project_id=' + encodeURIComponent(projectId)) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.success) { + const currentLoc = locationSelect.value; + const validLoc = data.locations.includes(currentLoc) ? currentLoc : ''; + rebuildLocationDropdown(data.locations, validLoc); + } else { + console.error('Failed to load locations:', data.error); + } + }) + .catch(function (err) { console.error('Error fetching locations:', err); }); + } + + function restoreAllLocations() { + while (locationSelect.options.length > 1) { locationSelect.remove(1); } + for (let i = 1; i < allLocationOptions.length; i++) { + const opt = document.createElement('option'); + opt.value = allLocationOptions[i].value; + opt.textContent = allLocationOptions[i].text; + if (allLocationOptions[i].value === initialLocationValue) opt.selected = true; + locationSelect.appendChild(opt); + } + } + + projectSelect.addEventListener('change', function () { + if (this.value) { + loadLocationsForProject(this.value); + } else { + restoreAllLocations(); + } }); }());