mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-10-02 10:53:13 +00:00
Anchors to the current week's Monday and shows 14 consecutive days. Updates /oncall next to jump forward by 2 weeks accordingly. Closes #1
132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""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 <date>` — Pick up a shift\n"
|
|
"`/oncall drop <date>` — Drop your shift (marks it open)\n"
|
|
"`/oncall swap <date> @person` — Swap your shift with someone\n"
|
|
"`/oncall register <extension>` — 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)}}]
|