diff --git a/app/routes/events/crud.py b/app/routes/events/crud.py index c6bf6ef..7ab3e96 100644 --- a/app/routes/events/crud.py +++ b/app/routes/events/crud.py @@ -11,7 +11,7 @@ from app.extensions import db from app.models.assignment import Assignment from app.models.equipment import EquipmentCategory, EquipmentItem, EquipmentItemStatus, EquipmentType -from app.models.event import Event, EventSpot, EventStatus, EventTemplate, EventType +from app.models.event import Event, EventSpot, EventStatus, EventTemplate, EventType, spot_qualifications from app.models.master_event import MasterEvent from app.models.qualification import Qualification from app.models.user import UserAccount @@ -86,6 +86,8 @@ def _parse_index_filters() -> dict: raw_types = request.args.get("types", "") active_types = [t for t in raw_types.split(",") if t in _ALL_EVENT_TYPES] + for_me = request.args.get("for_me") == "1" and current_user.has_permission("event.assign_own") + return { "show_archived": show_archived, "page": page, @@ -94,6 +96,7 @@ def _parse_index_filters() -> dict: "sort_dir": sort_dir, "active_me": active_me, "active_types": active_types, + "for_me": for_me, } @@ -168,6 +171,44 @@ def _build_eligible_spot_map(events: list[Event]) -> dict[int, list[tuple[int, s return result +def _eligible_event_ids_for_user(user: UserAccount) -> list[int]: + """Return a list of event IDs where the user has at least one unoccupied, fillable spot.""" + fillable_ids = user_fillable_qual_ids(user) + + # Find deleted qual IDs so we can ignore them (match existing eligibility logic) + deleted_qual_ids = { + q.id for q in db.session.scalars(db.select(Qualification).where(Qualification.is_deleted.is_(True))).all() + } + + # Fetch all (spot_id, event_id, qual_id) for unoccupied spots + rows = db.session.execute( + db.select( + EventSpot.id.label("spot_id"), + EventSpot.event_id, + spot_qualifications.c.qualification_id, + ) + .outerjoin(Assignment, Assignment.spot_id == EventSpot.id) + .outerjoin(spot_qualifications, spot_qualifications.c.spot_id == EventSpot.id) + .where(Assignment.id.is_(None)) + ).all() + + spot_event: dict[int, int] = {} + spot_quals: dict[int, set[int]] = {} + for spot_id, event_id, qual_id in rows: + spot_event[spot_id] = event_id + if spot_id not in spot_quals: + spot_quals[spot_id] = set() + if qual_id is not None and qual_id not in deleted_qual_ids: + spot_quals[spot_id].add(qual_id) + + eligible_event_ids: set[int] = set() + for spot_id, required_qual_ids in spot_quals.items(): + if all(qid in fillable_ids for qid in required_qual_ids): + eligible_event_ids.add(spot_event[spot_id]) + + return list(eligible_event_ids) if eligible_event_ids else [-1] + + # ── List ────────────────────────────────────────────────────────────────────── @@ -200,6 +241,10 @@ def index() -> str: else: query = query.where(db.false()) + if f["for_me"]: + eligible_ids = _eligible_event_ids_for_user(current_user) + query = query.where(Event.id.in_(eligible_ids)) + query = _apply_index_order(query, f["sort_col"], f["sort_dir"]) pagination = db.paginate(query, page=f["page"], per_page=PER_PAGE, error_out=False) events = pagination.items @@ -232,6 +277,7 @@ def index() -> str: eligible_spot_map=_build_eligible_spot_map(events), active_named_mes=active_named_mes, status_colors=STATUS_BADGE_COLORS, + for_me=f["for_me"], ) diff --git a/app/static/js/events-index.js b/app/static/js/events-index.js index cd9a0bb..b913d02 100644 --- a/app/static/js/events-index.js +++ b/app/static/js/events-index.js @@ -12,24 +12,16 @@ var ACTIVE_STATUSES = cfg.activeStatuses || []; var CLAIM_BASE = cfg.claimBase || ""; var ACTIVE_ME_NAME = cfg.activeMeName || ""; + var FOR_ME = cfg.forMe || false; var STORAGE_VIEW = "medcover_events_view"; - var STORAGE_ELIG = "medcover_events_elig"; var STORAGE_DATE = "medcover_events_cal_date"; var calendarInitialized = false; var calendar = null; var allCalendarEvents = null; - var eligFilter = false; var currentCalDate = null; - // ── Per-page JS filter (elig only — status + ME are server-side) ── - - function loadEligFilter() { - try { return localStorage.getItem(STORAGE_ELIG) === "1"; } catch(e) { return false; } - } - function saveEligFilter(v) { localStorage.setItem(STORAGE_ELIG, v ? "1" : "0"); } - function loadCalendarDate() { try { return localStorage.getItem(STORAGE_DATE) || null; } catch(e) { return null; } } @@ -42,19 +34,9 @@ return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day; } - // ── Table row visibility (elig only — status + ME are server-side) ── + // ── Table row visibility (status + ME + elig are all server-side) ── function applyLocalFilters() { - var tbody = document.querySelector("#events-table tbody"); - if (!tbody) return; - var visibleCount = 0; - tbody.querySelectorAll("tr").forEach(function (row) { - var eligOk = !eligFilter || row.dataset.eligible === "1"; - row.style.display = eligOk ? "" : "none"; - if (eligOk) visibleCount++; - }); - var emptyMsg = document.getElementById("table-empty-msg"); - if (emptyMsg) emptyMsg.classList.toggle('d-none', visibleCount > 0); if (calendarInitialized && calendar) calendar.refetchEvents(); _saveEventNav(); } @@ -76,17 +58,6 @@ } catch(e) {} } - function toggleEligFilter() { - eligFilter = !eligFilter; - saveEligFilter(eligFilter); - var btn = document.getElementById("btn-elig-filter"); - if (btn) { - btn.classList.toggle("active", eligFilter); - btn.blur(); - } - applyLocalFilters(); - } - // ── ME filter navigation (server-side; select triggers URL change) ────── function navigateToMe(meId) { @@ -138,7 +109,7 @@ } successCallback(allCalendarEvents.filter(function (e) { var statusOk = ACTIVE_STATUSES.includes(e.extendedProps.status_key); - var eligOk = !eligFilter || e.extendedProps.eligible; + var eligOk = !FOR_ME || e.extendedProps.eligible; var meOk = !ACTIVE_ME_NAME || (e.extendedProps.me_name || "") === ACTIVE_ME_NAME; return statusOk && eligOk && meOk; })); @@ -240,29 +211,6 @@ // ── Init ────────────────────────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", function () { - eligFilter = loadEligFilter(); - var eligBtn = document.getElementById("btn-elig-filter"); - if (eligBtn) { - eligBtn.classList.toggle("active", eligFilter); - var eligTouchStartY = 0; - var eligTouchFired = false; - eligBtn.addEventListener("touchstart", function (e) { - eligTouchStartY = e.touches[0].clientY; - }, { passive: true }); - eligBtn.addEventListener("touchend", function (e) { - var dy = Math.abs(e.changedTouches[0].clientY - eligTouchStartY); - if (dy > 10) return; - eligTouchFired = true; - e.preventDefault(); - toggleEligFilter(); - setTimeout(function () { eligTouchFired = false; }, 500); - }, { passive: false }); - eligBtn.addEventListener("click", function () { - if (eligTouchFired) return; - toggleEligFilter(); - }); - } - applyLocalFilters(); var saved = localStorage.getItem(STORAGE_VIEW) || "table"; @@ -329,5 +277,4 @@ window.navigateToMe = navigateToMe; window.clearSelection = clearSelection; window.submitBulk = submitBulk; - window.toggleEligFilter = toggleEligFilter; })(); diff --git a/app/templates/events/index.html b/app/templates/events/index.html index 88098ff..2782086 100644 --- a/app/templates/events/index.html +++ b/app/templates/events/index.html @@ -13,6 +13,7 @@ sort=sort_col if sort_col != 'start' else None, dir=sort_dir if sort_col != 'start' or sort_dir != 'asc' else None, archived=1 if (archived if archived is not none else show_archived) else None, + for_me=1 if for_me else None, ) -}} {%- endmacro %} @@ -68,7 +69,7 @@

Akce {{ help_icon("Akce procházejí těmito fázemi:\n• Konc {%- else -%} {%- set new_statuses = active_statuses + [toggle_status] -%} {%- endif -%} - {{ url_for('events.index', statuses=new_statuses|join(','), types=active_types|join(',') if active_types|length < all_event_types|length else None, me_id=active_me.id if active_me else None, sort=sort_col if sort_col != 'start' else None, dir=sort_dir if sort_col != 'start' or sort_dir != 'asc' else None, archived=1 if show_archived else None) }} + {{ url_for('events.index', statuses=new_statuses|join(','), types=active_types|join(',') if active_types|length < all_event_types|length else None, me_id=active_me.id if active_me else None, sort=sort_col if sort_col != 'start' else None, dir=sort_dir if sort_col != 'start' or sort_dir != 'asc' else None, archived=1 if show_archived else None, for_me=1 if for_me else None) }} {%- endmacro %} {# Type filter toggle macro #} @@ -79,7 +80,7 @@

Akce {{ help_icon("Akce procházejí těmito fázemi:\n• Konc {%- else -%} {%- set new_types = active_types + [toggle_type] -%} {%- endif -%} - {{ url_for('events.index', types=new_types|join(',') if new_types|length < all_event_types|length else None, statuses=active_statuses|join(','), me_id=active_me.id if active_me else None, sort=sort_col if sort_col != 'start' else None, dir=sort_dir if sort_col != 'start' or sort_dir != 'asc' else None, archived=1 if show_archived else None) }} + {{ url_for('events.index', types=new_types|join(',') if new_types|length < all_event_types|length else None, statuses=active_statuses|join(','), me_id=active_me.id if active_me else None, sort=sort_col if sort_col != 'start' else None, dir=sort_dir if sort_col != 'start' or sort_dir != 'asc' else None, archived=1 if show_archived else None, for_me=1 if for_me else None) }} {%- endmacro %} {# Sort URL: preserve statuses + types + me_id + archived, toggle sort col/dir #} @@ -88,7 +89,7 @@

Akce {{ help_icon("Akce procházejí těmito fázemi:\n• Konc {%- if sort_col == col -%} {%- set new_dir = 'desc' if sort_dir == 'asc' else 'asc' -%} {%- endif -%} - {{ url_for('events.index', sort=col, dir=new_dir, statuses=active_statuses|join(','), types=active_types|join(',') if active_types|length < all_event_types|length else None, me_id=active_me.id if active_me else None, archived=1 if show_archived else None) }} + {{ url_for('events.index', sort=col, dir=new_dir, statuses=active_statuses|join(','), types=active_types|join(',') if active_types|length < all_event_types|length else None, me_id=active_me.id if active_me else None, archived=1 if show_archived else None, for_me=1 if for_me else None) }} {%- endmacro %} {% macro sort_icon(col) %} {%- if sort_col == col -%} @@ -108,7 +109,8 @@

Akce {{ help_icon("Akce procházejí těmito fázemi:\n• Konc Dokončena Zrušena {% if current_user.has_permission('event.assign_own') %} - + Pro mě {% endif %}