Fix weekend day shift commands and admin 3CX routing

- Add _find_employee_shift() to check both day/night on weekends
- Drop/swap now correctly find and operate on weekend day shifts
- Pick finds first available shift type on weekends
- Admin override/open/clear update 3CX for same-day active shifts
This commit is contained in:
Adam Moussa 2026-05-12 16:46:11 -04:00
parent eb8f397b9e
commit 69220afb9a

View file

@ -100,6 +100,24 @@ def _is_active_shift_type(shift_type: str) -> bool:
return shift_type == "night" return shift_type == "night"
def _find_employee_shift(schedule, date_str, day_name, employee_ext):
"""Find which shift type an employee is assigned to on a given date.
On weekends, checks both day and night shifts. Returns (ext, name, source, shift_type)
or None if not found on any shift.
"""
if day_name in WEEKEND_DAYS:
for st in ("day", "night"):
ext, name, source = schedule.resolve_shift(date_str, day_name, st)
if ext == employee_ext:
return ext, name, source, st
return None
ext, name, source = schedule.resolve_shift(date_str, day_name, "night")
if ext == employee_ext:
return ext, name, source, "night"
return None
def create_app( def create_app(
bot_token: str, signing_secret: str, schedule_channel: str | None = None bot_token: str, signing_secret: str, schedule_channel: str | None = None
) -> App: ) -> App:
@ -380,7 +398,19 @@ def create_app(
return return
day_name = date.strftime("%A") day_name = date.strftime("%A")
ext, name, source = schedule.resolve_shift(date_str, day_name)
# On weekends, find the first available shift (day then night)
shift_type = "night"
if day_name in WEEKEND_DAYS:
for st in ("day", "night"):
ext, name, source = schedule.resolve_shift(date_str, day_name, st)
if source == "available":
shift_type = st
break
else:
ext, name, source = schedule.resolve_shift(date_str, day_name)
else:
ext, name, source = schedule.resolve_shift(date_str, day_name)
# Already assigned to someone else (not open) # Already assigned to someone else (not open)
if source in ("weekly", "override") and ext != FALLBACK_EXTENSION: if source in ("weekly", "override") and ext != FALLBACK_EXTENSION:
@ -389,15 +419,22 @@ def create_app(
) )
return return
schedule.set_override(date_str, employee["extension"], employee["name"]) schedule.set_override(
date_str, employee["extension"], employee["name"], shift_type
)
if is_today(date_str) and _is_active_shift_type("night"): if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(employee["extension"]) _update_3cx_routing(employee["extension"])
respond(text=f"You picked up the shift for *{date.strftime('%A, %b %-d')}*.") respond(text=f"You picked up the shift for *{date.strftime('%A, %b %-d')}*.")
blocks = build_shift_change_message( 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,
) )
try: try:
client.chat_postMessage( client.chat_postMessage(
@ -435,22 +472,28 @@ def create_app(
return return
day_name = date.strftime("%A") day_name = date.strftime("%A")
ext, name, source = schedule.resolve_shift(date_str, day_name) found = _find_employee_shift(
schedule, date_str, day_name, employee["extension"]
)
if ext != employee["extension"]: if not found:
ext, name, _source = schedule.resolve_shift(date_str, day_name)
respond(text=f"That's not your shift — it belongs to {name} (Ext {ext}).") respond(text=f"That's not your shift — it belongs to {name} (Ext {ext}).")
return return
schedule.mark_open(date_str) ext, name, source, shift_type = found
schedule.mark_open(date_str, shift_type)
if is_today(date_str) and _is_active_shift_type("night"): if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(FALLBACK_EXTENSION) _update_3cx_routing(FALLBACK_EXTENSION)
respond( respond(
text=f"You dropped the shift for *{date.strftime('%A, %b %-d')}*. It's now open for pickup." text=f"You dropped the shift for *{date.strftime('%A, %b %-d')}*. It's now open for pickup."
) )
blocks = build_shift_change_message(user_id, date_str, "dropped", ext, name) blocks = build_shift_change_message(
user_id, date_str, "dropped", ext, name, shift_type=shift_type
)
try: try:
client.chat_postMessage( client.chat_postMessage(
channel=channel_id, blocks=blocks, text=f"Shift dropped for {date_str}" channel=channel_id, blocks=blocks, text=f"Shift dropped for {date_str}"
@ -486,14 +529,19 @@ def create_app(
return return
day_name = date.strftime("%A") day_name = date.strftime("%A")
ext, name, source = schedule.resolve_shift(date_str, day_name) found = _find_employee_shift(
schedule, date_str, day_name, employee["extension"]
)
if ext != employee["extension"]: if not found:
ext, name, _source = schedule.resolve_shift(date_str, day_name)
respond( respond(
text=f"That's not your shift — it belongs to {name} (Ext {ext}). You can only swap your own shifts." text=f"That's not your shift — it belongs to {name} (Ext {ext}). You can only swap your own shifts."
) )
return return
_ext, _name, _source, shift_type = found
# Resolve target user — could be <@U12345> or an extension number # Resolve target user — could be <@U12345> or an extension number
target_text = parts[2].strip() target_text = parts[2].strip()
slack_id_match = re.match(r"<@(\w+)(?:\|[^>]*)?>", target_text) slack_id_match = re.match(r"<@(\w+)(?:\|[^>]*)?>", target_text)
@ -511,9 +559,9 @@ def create_app(
respond(text=f"Extension `{target_text}` not found in the roster.") respond(text=f"Extension `{target_text}` not found in the roster.")
return return
schedule.set_override(date_str, target["extension"], target["name"]) schedule.set_override(date_str, target["extension"], target["name"], shift_type)
if is_today(date_str) and _is_active_shift_type("night"): if is_today(date_str) and _is_active_shift_type(shift_type):
_update_3cx_routing(target["extension"]) _update_3cx_routing(target["extension"])
blocks = build_shift_change_message( blocks = build_shift_change_message(
@ -522,6 +570,7 @@ def create_app(
"swapped", "swapped",
target["extension"], target["extension"],
target["name"], target["name"],
shift_type=shift_type,
) )
respond( respond(
text=f"Swapped *{date.strftime('%A, %b %-d')}* to {target['name']} (Ext {target['extension']})." text=f"Swapped *{date.strftime('%A, %b %-d')}* to {target['name']} (Ext {target['extension']})."
@ -572,6 +621,8 @@ def create_app(
return return
date_str = date.strftime("%Y-%m-%d") date_str = date.strftime("%Y-%m-%d")
schedule.set_override(date_str, employee["extension"], employee["name"]) schedule.set_override(date_str, employee["extension"], employee["name"])
if is_today(date_str) and _is_active_shift_type("night"):
_update_3cx_routing(employee["extension"])
respond( respond(
text=f"Override set: *{date.strftime('%A, %b %-d')}* → {employee['name']} (Ext {ext})" text=f"Override set: *{date.strftime('%A, %b %-d')}* → {employee['name']} (Ext {ext})"
) )
@ -587,6 +638,8 @@ def create_app(
return return
date_str = date.strftime("%Y-%m-%d") date_str = date.strftime("%Y-%m-%d")
schedule.mark_open(date_str) schedule.mark_open(date_str)
if is_today(date_str) and _is_active_shift_type("night"):
_update_3cx_routing(FALLBACK_EXTENSION)
respond(text=f"*{date.strftime('%A, %b %-d')}* marked as open.") respond(text=f"*{date.strftime('%A, %b %-d')}* marked as open.")
_refresh_schedule_post(client) _refresh_schedule_post(client)
@ -600,6 +653,12 @@ def create_app(
return return
date_str = date.strftime("%Y-%m-%d") date_str = date.strftime("%Y-%m-%d")
schedule.remove_override(date_str) schedule.remove_override(date_str)
if is_today(date_str) and _is_active_shift_type("night"):
day_name = date.strftime("%A")
ext, _name, _source = schedule.resolve_shift(
date_str, day_name, "night"
)
_update_3cx_routing(ext)
respond( respond(
text=f"Override cleared for *{date.strftime('%A, %b %-d')}* — reverted to weekly schedule." text=f"Override cleared for *{date.strftime('%A, %b %-d')}* — reverted to weekly schedule."
) )