"""Slack Block Kit message builders for shift schedule display.""" from datetime import datetime, timedelta from zoneinfo import ZoneInfo EASTERN = ZoneInfo("America/New_York") DAY_ORDER = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] def _format_shift_line(date: datetime, ext: str, name: str, source: str, is_today: bool) -> str: day_label = date.strftime("%a %b %-d") if is_today: day_label = f"*{day_label} (today)*" if source == "available": return f":large_yellow_circle: {day_label} — *Available*" if source == "override": return f":large_blue_circle: {day_label} — {name} (Ext {ext}) _[picked up]_" return f":white_circle: {day_label} — {name} (Ext {ext})" def build_week_schedule(schedule, start_date: datetime | None = None) -> list[dict]: """Build Block Kit blocks showing the two-week schedule. Args: schedule: ShiftSchedule instance start_date: Monday of the first week to show. Defaults to current week's Monday. """ now = datetime.now(EASTERN) if start_date is None: start_date = now - timedelta(days=now.weekday()) # Monday of this week start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) today_str = now.strftime("%Y-%m-%d") start_label = start_date.strftime("%b %-d") end_label = (start_date + timedelta(days=13)).strftime("%b %-d") # Build lines for both weeks, separated by a blank line lines = [] open_dates = [] for i in range(14): if i == 7: lines.append("") # visual separator between weeks date = start_date + timedelta(days=i) date_str = date.strftime("%Y-%m-%d") day_name = DAY_ORDER[i % 7] ext, name, source = schedule.resolve_shift(date_str, day_name) is_today = date_str == today_str lines.append(_format_shift_line(date, ext, name, source, is_today)) if source == "available": open_dates.append(date_str) blocks = [ { "type": "header", "text": {"type": "plain_text", "text": f"After-Hours Schedule — {start_label} to {end_label}"}, }, { "type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}, }, ] # Add pickup buttons for open shifts (Slack max 5 per actions block) if open_dates: blocks.append({"type": "divider"}) elements = [] for date_str in open_dates: dt = datetime.strptime(date_str, "%Y-%m-%d") elements.append( { "type": "button", "text": {"type": "plain_text", "text": f"Pick up {dt.strftime('%a %b %-d')}"}, "action_id": f"pickup_{date_str}", "style": "primary", } ) # Slack allows max 5 buttons per actions block — use multiple blocks for chunk_start in range(0, len(elements), 5): blocks.append({"type": "actions", "elements": elements[chunk_start:chunk_start + 5]}) return blocks def build_shift_change_message( user_id: str, date_str: str, action: str, ext: str, name: str ) -> list[dict]: """Build a channel notification for a shift change.""" dt = datetime.strptime(date_str, "%Y-%m-%d") day_label = dt.strftime("%A, %b %-d") if action == "picked_up": text = f":white_check_mark: <@{user_id}> picked up the shift for *{day_label}* (Ext {ext})" elif action == "dropped": text = f":warning: <@{user_id}> dropped the shift for *{day_label}* — it's now *Available*" elif action == "swapped": text = f":arrows_counterclockwise: <@{user_id}> swapped into the shift for *{day_label}* (Ext {ext})" else: text = f"Shift updated for *{day_label}*: {name} (Ext {ext})" return [{"type": "section", "text": {"type": "mrkdwn", "text": text}}] def build_help_blocks() -> list[dict]: """Build help message blocks.""" text = ( "*After-Hours Shift Manager*\n\n" "`/oncall` — Show the two-week schedule\n" "`/oncall next` — Show the following two weeks\n" "`/oncall pick ` — Pick up a shift\n" "`/oncall drop ` — Drop your shift (marks it open)\n" "`/oncall swap @person` — Swap your shift with someone\n" "`/oncall register ` — Link your Slack account to your extension\n" "`/oncall roster` — Show all registered employees\n" "`/oncall help` — Show this help message\n\n" "_Dates can be: today, tomorrow, monday, friday, 4/5, 2026-04-05_" ) return [{"type": "section", "text": {"type": "mrkdwn", "text": text}}] def build_roster_blocks(roster: list[dict]) -> list[dict]: """Build roster display blocks.""" if not roster: return [{"type": "section", "text": {"type": "mrkdwn", "text": "No employees in the roster."}}] lines = ["*On-Call Roster*\n"] for emp in sorted(roster, key=lambda x: x["SK"]): slack = f"<@{emp['slack_user_id']}>" if emp.get("slack_user_id") else "_not linked_" lines.append(f"Ext {emp['SK']} — {emp.get('name', 'Unknown')} ({slack})") return [{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}}]