Disallow past shifts and add day/night labels (#43, #42)

- Reject /oncall pick and /oncall drop for past dates
- Show ephemeral error when stale pickup buttons are clicked
- Hide pickup buttons for dates in the past
- Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to
  schedule lines, pickup buttons, and shift change notifications
This commit is contained in:
Adam Moussa 2026-05-12 15:48:35 -04:00
parent 9dd7cbd0f8
commit e9b55d357b
2 changed files with 44 additions and 12 deletions

View file

@ -8,6 +8,12 @@ EASTERN = ZoneInfo("America/New_York")
WEEKEND_DAYS = {"Saturday", "Sunday"}
SHIFT_LABELS = {
"day": "Day (8am–5pm)",
"night": "Night (5pm–8am)",
}
def _format_shift_line(
date: datetime,
ext: str,
@ -17,8 +23,8 @@ def _format_shift_line(
shift_type: str = "night",
) -> str:
day_label = date.strftime("%a %b %-d")
if shift_type == "day":
day_label += " (Day 8a–5p)"
if shift_type in SHIFT_LABELS:
day_label += f" {SHIFT_LABELS[shift_type]}"
if is_today:
day_label = f"*{day_label} (today)*"
@ -56,17 +62,19 @@ def build_week_schedule(schedule, start_date: datetime | None = None) -> list[di
day_name = date.strftime("%A")
is_today = date_str == today_str
is_past = date_str < today_str
# Weekend day shift (8am–5pm)
if day_name in WEEKEND_DAYS:
ext, name, source = schedule.resolve_shift(date_str, day_name, "day")
lines.append(_format_shift_line(date, ext, name, source, is_today, "day"))
if source == "available":
if source == "available" and not is_past:
open_shifts.append((date_str, "day"))
# After-hours (night) shift
ext, name, source = schedule.resolve_shift(date_str, day_name)
lines.append(_format_shift_line(date, ext, name, source, is_today))
if source == "available":
if source == "available" and not is_past:
open_shifts.append((date_str, "night"))
blocks = [
@ -89,14 +97,14 @@ def build_week_schedule(schedule, start_date: datetime | None = None) -> list[di
elements = []
for date_str, shift_type in open_shifts:
dt = datetime.strptime(date_str, "%Y-%m-%d")
suffix = " (Day)" if shift_type == "day" else ""
label = "Day" if shift_type == "day" else "Night"
action_suffix = "_day" if shift_type == "day" else ""
elements.append(
{
"type": "button",
"text": {
"type": "plain_text",
"text": f"Pick up {dt.strftime('%a %b %-d')}{suffix}",
"text": f"Pick up {dt.strftime('%a %b %-d')} {label}",
},
"action_id": f"pickup_{date_str}{action_suffix}",
"style": "primary",
@ -112,20 +120,26 @@ def build_week_schedule(schedule, start_date: datetime | None = None) -> list[di
def build_shift_change_message(
user_id: str, date_str: str, action: str, ext: str, name: str
user_id: str,
date_str: str,
action: str,
ext: str,
name: str,
shift_type: str = "night",
) -> 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")
type_label = f" ({SHIFT_LABELS.get(shift_type, shift_type)})" if shift_type == "day" else ""
if action == "picked_up":
text = f":white_check_mark: <@{user_id}> picked up the shift for *{day_label}* (Ext {ext})"
text = f":white_check_mark: <@{user_id}> picked up the *{day_label}*{type_label} shift (Ext {ext})"
elif action == "dropped":
text = f":warning: <@{user_id}> dropped the shift for *{day_label}* — it's now *Available*"
text = f":warning: <@{user_id}> dropped the *{day_label}*{type_label} shift — it's now *Available*"
elif action == "swapped":
text = f":arrows_counterclockwise: <@{user_id}> swapped into the shift for *{day_label}* (Ext {ext})"
text = f":arrows_counterclockwise: <@{user_id}> swapped into the *{day_label}*{type_label} shift (Ext {ext})"
else:
text = f"Shift updated for *{day_label}*: {name} (Ext {ext})"
text = f"Shift updated for *{day_label}*{type_label}: {name} (Ext {ext})"
return [{"type": "section", "text": {"type": "mrkdwn", "text": text}}]

View file

@ -153,6 +153,15 @@ def create_app(
user_id = body["user"]["id"]
channel_id = body["channel"]["id"]
today_str = datetime.now(EASTERN).strftime("%Y-%m-%d")
if date_str < today_str:
client.chat_postEphemeral(
channel=channel_id,
user=user_id,
text="That shift has already passed and can't be picked up.",
)
return
employee = schedule.get_employee_by_slack_id(user_id)
if not employee:
client.chat_postEphemeral(
@ -177,7 +186,8 @@ def create_app(
_update_3cx_routing(employee["extension"])
blocks = build_shift_change_message(
user_id, date_str, "picked_up", employee["extension"], employee["name"]
user_id, date_str, "picked_up", employee["extension"], employee["name"],
shift_type=shift_type,
)
respond(
response_type="in_channel",
@ -345,6 +355,10 @@ def create_app(
return
date_str = date.strftime("%Y-%m-%d")
if date_str < datetime.now(EASTERN).strftime("%Y-%m-%d"):
respond(text="You can't pick up a shift in the past.")
return
day_name = date.strftime("%A")
ext, name, source = schedule.resolve_shift(date_str, day_name)
@ -396,6 +410,10 @@ def create_app(
return
date_str = date.strftime("%Y-%m-%d")
if date_str < datetime.now(EASTERN).strftime("%Y-%m-%d"):
respond(text="You can't drop a shift in the past.")
return
day_name = date.strftime("%A")
ext, name, source = schedule.resolve_shift(date_str, day_name)