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čenaZrušena
{% if current_user.has_permission('event.assign_own') %}
-
+ Pro mě
{% endif %}