"""Calendar grid, day panels, month navigation, calendar-specific helpers.""" from __future__ import annotations from shared.sx.helpers import ( call_url, sx_call, render_to_sx_with_env, post_admin_header_sx, ) from shared.sx.parser import SxExpr from .utils import ( _clear_deeper_oob, _ensure_container_nav, _entry_state_badge_html, _list_container, ) # --------------------------------------------------------------------------- # Post header helpers # --------------------------------------------------------------------------- async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the post-level header row — delegates to shared sx helper.""" from shared.sx.helpers import post_header_sx return await post_header_sx(ctx, oob=oob) def _post_nav_sx(ctx: dict) -> str: """Post desktop nav: calendar links + container nav (markets, etc.).""" from quart import url_for, g calendars = ctx.get("calendars") or [] select_colours = ctx.get("select_colours", "") current_cal_slug = getattr(g, "calendar_slug", None) parts = [] for cal in calendars: cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "") cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "") href = url_for("calendar.get", calendar_slug=cal_slug) is_sel = (cal_slug == current_cal_slug) parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar", label=cal_name, select_colours=select_colours, is_selected=is_sel)) # Container nav fragments (markets, etc.) container_nav = ctx.get("container_nav", "") if container_nav: parts.append(container_nav) # Admin cog → blog admin for this post (cross-domain, no HTMX) rights = ctx.get("rights") or {} has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) if has_admin: post = ctx.get("post") or {} slug = post.get("slug", "") styles = ctx.get("styles") or {} nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "") select_colours = ctx.get("select_colours", "") admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/") aclass = f"{nav_btn} {select_colours}".strip() or ( "justify-center cursor-pointer flex flex-row items-center gap-2 " "rounded bg-stone-200 text-black p-3" ) parts.append( f'' ) return "".join(parts) # --------------------------------------------------------------------------- # Calendars header # --------------------------------------------------------------------------- def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the calendars section header row.""" from quart import url_for link_href = url_for("calendars.home") return sx_call("menu-row-sx", id="calendars-row", level=3, link_href=link_href, link_label_content=sx_call("events-calendars-label"), child_id="calendars-header-child", oob=oob) # --------------------------------------------------------------------------- # Calendar header # --------------------------------------------------------------------------- def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build a single calendar's header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") cal_name = getattr(calendar, "name", "") cal_desc = getattr(calendar, "description", "") or "" link_href = url_for("calendar.get", calendar_slug=cal_slug) label_html = sx_call("events-calendar-label", name=cal_name, description=cal_desc) # Desktop nav: slots + admin nav_html = _calendar_nav_sx(ctx) return sx_call("menu-row-sx", id="calendar-row", level=3, link_href=link_href, link_label_content=label_html, nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob) def _calendar_nav_sx(ctx: dict) -> str: """Calendar desktop nav: Slots + admin link.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) select_colours = ctx.get("select_colours", "") parts = [] slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug) parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock", label="Slots", select_colours=select_colours)) if is_admin: admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug) parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog", select_colours=select_colours)) return "(<> " + " ".join(parts) + ")" if parts else "" # --------------------------------------------------------------------------- # Day header # --------------------------------------------------------------------------- def _day_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build day detail header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_date = ctx.get("day_date") if not day_date: return "" link_href = url_for( "calendar.day.show_day", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) label_html = sx_call("events-day-label", date_str=day_date.strftime("%A %d %B %Y")) nav_html = _day_nav_sx(ctx) return sx_call("menu-row-sx", id="day-row", level=4, link_href=link_href, link_label_content=label_html, nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob) def _day_nav_sx(ctx: dict) -> str: """Day desktop nav: confirmed entries scrolling menu + admin link.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_date = ctx.get("day_date") confirmed_entries = ctx.get("confirmed_entries") or [] rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) parts = [] # Confirmed entries nav (scrolling menu) if confirmed_entries: entry_links = [] for entry in confirmed_entries: href = url_for( "calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, entry_id=entry.id, ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" entry_links.append(sx_call("events-day-entry-link", href=href, name=entry.name, time_str=f"{start}{end}")) inner = "".join(entry_links) parts.append(sx_call("events-day-entries-nav", inner=SxExpr(inner))) if is_admin and day_date: admin_href = url_for( "defpage_day_admin", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog")) return "".join(parts) # --------------------------------------------------------------------------- # Day admin header # --------------------------------------------------------------------------- def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build day admin header row.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_date = ctx.get("day_date") if not day_date: return "" link_href = url_for( "defpage_day_admin", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) return sx_call("menu-row-sx", id="day-admin-row", level=5, link_href=link_href, link_label="admin", icon="fa fa-cog", child_id="day-admin-header-child", oob=oob) # --------------------------------------------------------------------------- # Calendar admin header # --------------------------------------------------------------------------- def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build calendar admin header row with nav links.""" from quart import url_for calendar = ctx.get("calendar") cal_slug = getattr(calendar, "slug", "") if calendar else "" select_colours = ctx.get("select_colours", "") nav_parts = [] if cal_slug: for endpoint, label in [ ("defpage_slots_listing", "slots"), ("calendar.admin.calendar_description_edit", "description"), ]: href = url_for(endpoint, calendar_slug=cal_slug) nav_parts.append(sx_call("nav-link", href=href, label=label, select_colours=select_colours)) nav_html = "".join(nav_parts) return sx_call("menu-row-sx", id="calendar-admin-row", level=4, link_label="admin", icon="fa fa-cog", nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob) # --------------------------------------------------------------------------- # Markets header # --------------------------------------------------------------------------- def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the markets section header row.""" from quart import url_for link_href = url_for("defpage_events_markets") return sx_call("menu-row-sx", id="markets-row", level=3, link_href=link_href, link_label_content=sx_call("events-markets-label"), child_id="markets-header-child", oob=oob) # --------------------------------------------------------------------------- # Calendars main panel # --------------------------------------------------------------------------- def _calendars_main_panel_sx(ctx: dict) -> str: """Render the calendars list + create form panel.""" from quart import url_for rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) has_access = ctx.get("has_access") can_create = has_access("calendars.create_calendar") if callable(has_access) else is_admin csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") calendars = ctx.get("calendars") or [] form_html = "" if can_create: create_url = url_for("calendars.create_calendar") form_html = sx_call("crud-create-form", create_url=create_url, csrf=csrf, errors_id="cal-create-errors", list_id="calendars-list", placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar") list_html = _calendars_list_sx(ctx, calendars) return sx_call("crud-panel", form=SxExpr(form_html), list=SxExpr(list_html), list_id="calendars-list") def _calendars_list_sx(ctx: dict, calendars: list) -> str: """Render the calendars list items.""" from quart import url_for from shared.utils import route_prefix csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") prefix = route_prefix() if not calendars: return sx_call("empty-state", message="No calendars yet. Create one above.", cls="text-gray-500 mt-4") parts = [] for cal in calendars: cal_slug = getattr(cal, "slug", "") cal_name = getattr(cal, "name", "") href = prefix + url_for("calendar.get", calendar_slug=cal_slug) del_url = url_for("calendar.delete", calendar_slug=cal_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' parts.append(sx_call("crud-item", href=href, name=cal_name, slug=cal_slug, del_url=del_url, csrf_hdr=csrf_hdr, list_id="calendars-list", confirm_title="Delete calendar?", confirm_text="Entries will be hidden (soft delete)")) return "".join(parts) # --------------------------------------------------------------------------- # Calendar month grid # --------------------------------------------------------------------------- def _calendar_main_panel_html(ctx: dict) -> str: """Render the calendar month grid.""" from quart import url_for from quart import session as qsession calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") hx_select = ctx.get("hx_select_search", "#main-panel") styles = ctx.get("styles") or {} pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") year = ctx.get("year", 2024) month = ctx.get("month", 1) month_name = ctx.get("month_name", "") weekday_names = ctx.get("weekday_names", []) weeks = ctx.get("weeks", []) prev_month = ctx.get("prev_month", 1) prev_month_year = ctx.get("prev_month_year", year) next_month = ctx.get("next_month", 1) next_month_year = ctx.get("next_month_year", year) prev_year = ctx.get("prev_year", year - 1) next_year = ctx.get("next_year", year + 1) month_entries = ctx.get("month_entries") or [] user = ctx.get("user") qs = qsession if "qsession" not in ctx else ctx["qsession"] def nav_link(y, m): return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m) # Month navigation arrows nav_arrows = [] for label, yr, mn in [ ("\u00ab", prev_year, month), ("\u2039", prev_month_year, prev_month), ]: href = nav_link(yr, mn) nav_arrows.append(sx_call("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) nav_arrows.append(sx_call("events-calendar-month-label", month_name=month_name, year=str(year))) for label, yr, mn in [ ("\u203a", next_month_year, next_month), ("\u00bb", next_year, month), ]: href = nav_link(yr, mn) nav_arrows.append(sx_call("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) # Weekday headers wd_parts = [] for wd in weekday_names: wd_parts.append(sx_call("events-calendar-weekday", name=wd)) wd_html = "".join(wd_parts) # Day cells cells = [] for week in weeks: for day_cell in week: if isinstance(day_cell, dict): in_month = day_cell.get("in_month", True) is_today = day_cell.get("is_today", False) day_date = day_cell.get("date") else: in_month = getattr(day_cell, "in_month", True) is_today = getattr(day_cell, "is_today", False) day_date = getattr(day_cell, "date", None) cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs" if not in_month: cell_cls += " bg-stone-50 text-stone-400" if is_today: cell_cls += " ring-2 ring-blue-500 z-10 relative" # Day number link day_num_html = "" day_short_html = "" if day_date: day_href = url_for( "calendar.day.show_day", calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) day_short_html = sx_call("events-calendar-day-short", day_str=day_date.strftime("%a")) day_num_html = sx_call("events-calendar-day-num", pill_cls=pill_cls, href=day_href, num=str(day_date.day)) # Entry badges for this day entry_badges = [] if day_date: for e in month_entries: if e.start_at and e.start_at.date() == day_date: is_mine = ( (user and e.user_id == user.id) or (not user and e.session_id == qs.get("calendar_sid")) ) if e.state == "confirmed": bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800" else: bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700" state_label = (e.state or "pending").replace("_", " ") entry_badges.append(sx_call("events-calendar-entry-badge", bg_cls=bg_cls, name=e.name, state_label=state_label)) badges_html = "(<> " + "".join(entry_badges) + ")" if entry_badges else "" cells.append(sx_call("events-calendar-cell", cell_cls=cell_cls, day_short=SxExpr(day_short_html), day_num=SxExpr(day_num_html), badges=SxExpr(badges_html) if badges_html else None)) cells_html = "(<> " + "".join(cells) + ")" arrows_html = "(<> " + "".join(nav_arrows) + ")" wd_html = "(<> " + wd_html + ")" return sx_call("events-calendar-grid", arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html), cells=SxExpr(cells_html)) # --------------------------------------------------------------------------- # Day main panel # --------------------------------------------------------------------------- def _day_main_panel_html(ctx: dict) -> str: """Render the day entries table + add button.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" cal_slug = getattr(calendar, "slug", "") day_entries = ctx.get("day_entries") or [] day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") hx_select = ctx.get("hx_select_search", "#main-panel") styles = ctx.get("styles") or {} list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "") pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") rows_html = "" if day_entries: row_parts = [] for entry in day_entries: row_parts.append(_day_row_html(ctx, entry)) rows_html = "".join(row_parts) else: rows_html = sx_call("events-day-empty-row") add_url = url_for( "calendar.day.calendar_entries.add_form", calendar_slug=cal_slug, day=day, month=month, year=year, ) return sx_call("events-day-table", list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) def _day_row_html(ctx: dict, entry) -> str: """Render a single day table row.""" from quart import url_for calendar = ctx.get("calendar") cal_slug = getattr(calendar, "slug", "") day = ctx.get("day") month = ctx.get("month") year = ctx.get("year") hx_select = ctx.get("hx_select_search", "#main-panel") styles = ctx.get("styles") or {} pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") entry_href = url_for( "calendar.day.calendar_entries.calendar_entry.get", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id, ) # Name name_html = sx_call("events-day-row-name", href=entry_href, pill_cls=pill_cls, name=entry.name) # Slot/Time slot = getattr(entry, "slot", None) if slot: slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id) time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" slot_html = sx_call("events-day-row-slot", href=slot_href, pill_cls=pill_cls, slot_name=slot.name, time_str=f"({time_start}{time_end})") else: start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" slot_html = sx_call("events-day-row-time", start=start, end=end) # State state = getattr(entry, "state", "pending") or "pending" state_badge = _entry_state_badge_html(state) state_td = sx_call("events-day-row-state", state_id=f"entry-state-{entry.id}", badge=state_badge) # Cost cost = getattr(entry, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" cost_td = sx_call("events-day-row-cost", cost_str=cost_str) # Tickets tp = getattr(entry, "ticket_price", None) if tp is not None: tc = getattr(entry, "ticket_count", None) tc_str = f"{tc} tickets" if tc is not None else "Unlimited" tickets_td = sx_call("events-day-row-tickets", price_str=f"\u00a3{tp:.2f}", count_str=tc_str) else: tickets_td = sx_call("events-day-row-no-tickets") actions_td = sx_call("events-day-row-actions") return sx_call("events-day-row", tr_cls=tr_cls, name=name_html, slot=slot_html, state=state_td, cost=cost_td, tickets=tickets_td, actions=actions_td) # --------------------------------------------------------------------------- # Day admin main panel # --------------------------------------------------------------------------- def _day_admin_main_panel_html(ctx: dict) -> str: """Render day admin panel (placeholder nav).""" return sx_call("events-day-admin-panel") # --------------------------------------------------------------------------- # Calendar admin main panel # --------------------------------------------------------------------------- def _calendar_admin_main_panel_html(ctx: dict) -> str: """Render calendar admin config panel with description editor.""" from quart import url_for calendar = ctx.get("calendar") if not calendar: return "" csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") cal_slug = getattr(calendar, "slug", "") desc = getattr(calendar, "description", "") or "" hx_select = ctx.get("hx_select_search", "#main-panel") desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) description_html = _calendar_description_display_html(calendar, desc_edit_url) return sx_call("events-calendar-admin-panel", description_content=description_html, csrf=csrf, description=desc) def _calendar_description_display_html(calendar, edit_url: str) -> str: """Render calendar description display with edit button.""" desc = getattr(calendar, "description", "") or "" return sx_call("events-calendar-description-display", description=desc, edit_url=edit_url) # --------------------------------------------------------------------------- # Markets main panel # --------------------------------------------------------------------------- def _markets_main_panel_html(ctx: dict) -> str: """Render markets list + create form panel.""" from quart import url_for rights = ctx.get("rights") or {} is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False) has_access = ctx.get("has_access") can_create = has_access("markets.create_market") if callable(has_access) else is_admin csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") markets = ctx.get("markets") or [] form_html = "" if can_create: create_url = url_for("markets.create_market") form_html = sx_call("crud-create-form", create_url=create_url, csrf=csrf, errors_id="market-create-errors", list_id="markets-list", placeholder="e.g. Farm Shop, Bakery", btn_label="Add market") list_html = _markets_list_html(ctx, markets) return sx_call("crud-panel", form=SxExpr(form_html), list=SxExpr(list_html), list_id="markets-list") def _markets_list_html(ctx: dict, markets: list) -> str: """Render markets list items.""" from quart import url_for csrf_token = ctx.get("csrf_token") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") post = ctx.get("post") or {} slug = post.get("slug", "") if not markets: return sx_call("empty-state", message="No markets yet. Create one above.", cls="text-gray-500 mt-4") parts = [] for m in markets: m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "") m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "") market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/") del_url = url_for("markets.delete_market", market_slug=m_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' parts.append(sx_call("crud-item", href=market_href, name=m_name, slug=m_slug, del_url=del_url, csrf_hdr=csrf_hdr, list_id="markets-list", confirm_title="Delete market?", confirm_text="Products will be hidden (soft delete)")) return "".join(parts) # --------------------------------------------------------------------------- # Calendar admin helper # --------------------------------------------------------------------------- async def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: """Post-level admin row for events — delegates to shared helper.""" slug = (ctx.get("post") or {}).get("slug", "") return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)