mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 11:23:12 +00:00
777 lines
19 KiB
YAML
777 lines
19 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: After Hours Shift Manager
|
|
version: 0.1.0
|
|
description: >
|
|
Flask HTTP API on ECS Fargate. Slack `/oncall` stays on POST /slack/events
|
|
and is not part of this contract. The internal portal SPA calls /api/shifts
|
|
with a Cognito ID token from GET /api/auth/meals-token. Paychex calls
|
|
PUT/DELETE /roster with a shared bearer token. Error shape for portal
|
|
routes is `{ error: { code, message } }`. Health matches the portal BFF
|
|
`{ stage, sha }`. Lint with the same Redocly `extends: recommended` config
|
|
as internal-portal and meal-order-manager.
|
|
contact:
|
|
name: Sea Haven Engineering
|
|
license:
|
|
name: Proprietary
|
|
identifier: LicenseRef-SeaHaven
|
|
|
|
servers:
|
|
- url: /
|
|
description: After Hours ALB origin (VITE_SHIFTS_API_BASE)
|
|
|
|
tags:
|
|
- name: Runtime
|
|
description: Unauthenticated health
|
|
- name: Roster
|
|
description: Paychex hire and offboard
|
|
- name: Shifts
|
|
description: Employee and admin After Hours for the portal SPA
|
|
|
|
security: []
|
|
|
|
paths:
|
|
/api/health:
|
|
get:
|
|
operationId: getHealth
|
|
tags: [Runtime]
|
|
summary: Runtime health
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: Process is up
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Health"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/roster:
|
|
put:
|
|
operationId: putRoster
|
|
tags: [Roster]
|
|
summary: Upsert a roster row
|
|
security:
|
|
- rosterBearer: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RosterUpsert"
|
|
responses:
|
|
"200":
|
|
description: Row written
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RosterOk"
|
|
"400":
|
|
$ref: "#/components/responses/RosterError"
|
|
"401":
|
|
$ref: "#/components/responses/RosterError"
|
|
"503":
|
|
$ref: "#/components/responses/RosterError"
|
|
|
|
/roster/{extension}:
|
|
delete:
|
|
operationId: deleteRoster
|
|
tags: [Roster]
|
|
summary: Remove a roster row
|
|
security:
|
|
- rosterBearer: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/Extension"
|
|
responses:
|
|
"204":
|
|
description: Gone, including when the row was already missing
|
|
"400":
|
|
$ref: "#/components/responses/RosterError"
|
|
"401":
|
|
$ref: "#/components/responses/RosterError"
|
|
"503":
|
|
$ref: "#/components/responses/RosterError"
|
|
|
|
/api/shifts:
|
|
get:
|
|
operationId: getShifts
|
|
tags: [Shifts]
|
|
summary: Week snapshot for the signed-in employee
|
|
security:
|
|
- portalCognito: []
|
|
parameters:
|
|
- name: week
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [this, next]
|
|
default: this
|
|
responses:
|
|
"200":
|
|
description: Linked snapshot or unlinked Google account
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ShiftsSnapshot"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"503":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/pick:
|
|
post:
|
|
operationId: pickShift
|
|
tags: [Shifts]
|
|
summary: Pick up a shift or request late pickup
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ShiftDateBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
"409":
|
|
$ref: "#/components/responses/PortalError"
|
|
"503":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/drop:
|
|
post:
|
|
operationId: dropShift
|
|
tags: [Shifts]
|
|
summary: Drop a held shift
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ShiftDateBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
"409":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/swap:
|
|
post:
|
|
operationId: requestSwap
|
|
tags: [Shifts]
|
|
summary: Request a swap
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SwapBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
"409":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/swaps/{date}/{shiftType}/accept:
|
|
post:
|
|
operationId: acceptSwap
|
|
tags: [Shifts]
|
|
summary: Accept a pending swap
|
|
security:
|
|
- portalCognito: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/ShiftDate"
|
|
- $ref: "#/components/parameters/ShiftType"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
"409":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/swaps/{date}/{shiftType}/decline:
|
|
post:
|
|
operationId: declineSwap
|
|
tags: [Shifts]
|
|
summary: Decline a pending swap
|
|
security:
|
|
- portalCognito: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/ShiftDate"
|
|
- $ref: "#/components/parameters/ShiftType"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/override:
|
|
post:
|
|
operationId: adminOverride
|
|
tags: [Shifts]
|
|
summary: Assign a shift
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AdminOverrideBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/open:
|
|
post:
|
|
operationId: adminOpen
|
|
tags: [Shifts]
|
|
summary: Mark a shift open
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ShiftDateBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/clear:
|
|
post:
|
|
operationId: adminClear
|
|
tags: [Shifts]
|
|
summary: Clear an override
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ShiftDateBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/holidays:
|
|
post:
|
|
operationId: adminHolidayAdd
|
|
tags: [Shifts]
|
|
summary: Schedule a holiday day shift
|
|
security:
|
|
- portalCognito: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/HolidayBody"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"400":
|
|
$ref: "#/components/responses/PortalError"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
"409":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/holidays/{date}:
|
|
delete:
|
|
operationId: adminHolidayRemove
|
|
tags: [Shifts]
|
|
summary: Remove a holiday
|
|
security:
|
|
- portalCognito: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/ShiftDate"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/approve:
|
|
post:
|
|
operationId: adminPickupApprove
|
|
tags: [Shifts]
|
|
summary: Approve a late pickup
|
|
security:
|
|
- portalCognito: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/ShiftDate"
|
|
- $ref: "#/components/parameters/ShiftType"
|
|
- $ref: "#/components/parameters/Extension"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
"409":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/deny:
|
|
post:
|
|
operationId: adminPickupDeny
|
|
tags: [Shifts]
|
|
summary: Deny a late pickup
|
|
security:
|
|
- portalCognito: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/ShiftDate"
|
|
- $ref: "#/components/parameters/ShiftType"
|
|
- $ref: "#/components/parameters/Extension"
|
|
responses:
|
|
"200":
|
|
$ref: "#/components/responses/MutationOk"
|
|
"401":
|
|
$ref: "#/components/responses/PortalError"
|
|
"403":
|
|
$ref: "#/components/responses/PortalError"
|
|
"404":
|
|
$ref: "#/components/responses/PortalError"
|
|
|
|
components:
|
|
securitySchemes:
|
|
portalCognito:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
description: Portal Cognito ID token
|
|
rosterBearer:
|
|
type: http
|
|
scheme: bearer
|
|
description: Shared Paychex roster token
|
|
|
|
parameters:
|
|
ShiftDate:
|
|
name: date
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: date
|
|
ShiftType:
|
|
name: shiftType
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum: [day, night]
|
|
Extension:
|
|
name: extension
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
minLength: 1
|
|
|
|
responses:
|
|
MutationOk:
|
|
description: Mutation applied
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/MutationResult"
|
|
PortalError:
|
|
description: Portal JSON error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PortalErrorEnvelope"
|
|
RosterError:
|
|
description: Roster string error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RosterErrorBody"
|
|
|
|
schemas:
|
|
Health:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [stage, sha]
|
|
properties:
|
|
stage:
|
|
type: string
|
|
minLength: 1
|
|
description: Workspace stage (`dev`, `prod`, or `local`)
|
|
sha:
|
|
type: string
|
|
minLength: 1
|
|
description: Git SHA or `unknown` locally
|
|
|
|
PortalErrorEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [error]
|
|
properties:
|
|
error:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [code, message]
|
|
properties:
|
|
code:
|
|
type: string
|
|
minLength: 1
|
|
message:
|
|
type: string
|
|
minLength: 1
|
|
|
|
RosterErrorBody:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [error]
|
|
properties:
|
|
error:
|
|
type: string
|
|
minLength: 1
|
|
|
|
RosterUpsert:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [name, extension, slack_user_id]
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
extension:
|
|
type: string
|
|
minLength: 1
|
|
slack_user_id:
|
|
type: string
|
|
minLength: 1
|
|
email:
|
|
type: string
|
|
format: email
|
|
|
|
RosterOk:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [ok]
|
|
properties:
|
|
ok:
|
|
type: boolean
|
|
const: true
|
|
|
|
ShiftDateBody:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date]
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date
|
|
shiftType:
|
|
type: string
|
|
enum: [day, night, holiday]
|
|
|
|
SwapBody:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date, targetExtension]
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date
|
|
shiftType:
|
|
type: string
|
|
enum: [day, night]
|
|
targetExtension:
|
|
type: string
|
|
minLength: 1
|
|
note:
|
|
type: string
|
|
description: Optional. Stored after trim, and the trimmed value must be 500 characters or fewer.
|
|
|
|
AdminOverrideBody:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date, extension]
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date
|
|
extension:
|
|
type: string
|
|
minLength: 1
|
|
shiftType:
|
|
type: string
|
|
enum: [day, night]
|
|
|
|
HolidayBody:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date, slots, label]
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date
|
|
slots:
|
|
type: integer
|
|
minimum: 1
|
|
label:
|
|
type: string
|
|
minLength: 1
|
|
multiplier:
|
|
type: [number, string, "null"]
|
|
|
|
MutationResult:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [ok]
|
|
properties:
|
|
ok:
|
|
type: boolean
|
|
message:
|
|
type: string
|
|
latePickup:
|
|
type: boolean
|
|
repointed:
|
|
type: boolean
|
|
|
|
RosterPerson:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [extension, name, email]
|
|
properties:
|
|
extension:
|
|
type: string
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
slackUserId:
|
|
type: string
|
|
|
|
ShiftAssignee:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [extension, name]
|
|
properties:
|
|
extension:
|
|
type: string
|
|
name:
|
|
type: string
|
|
|
|
ShiftSlot:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- kind
|
|
- shiftType
|
|
- label
|
|
- slots
|
|
- openSlots
|
|
- multiplier
|
|
- assignees
|
|
- mine
|
|
- canPick
|
|
- canDrop
|
|
- latePickup
|
|
properties:
|
|
kind:
|
|
type: string
|
|
enum: [holiday, override, available, weekly]
|
|
shiftType:
|
|
type: string
|
|
enum: [day, night]
|
|
label:
|
|
type: string
|
|
slots:
|
|
type: integer
|
|
openSlots:
|
|
type: integer
|
|
multiplier:
|
|
type: number
|
|
assignees:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ShiftAssignee"
|
|
mine:
|
|
type: boolean
|
|
canPick:
|
|
type: boolean
|
|
canDrop:
|
|
type: boolean
|
|
latePickup:
|
|
type: boolean
|
|
|
|
ShiftDay:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date, dayName, slots]
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date
|
|
dayName:
|
|
type: string
|
|
slots:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ShiftSlot"
|
|
|
|
PendingSwap:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- date
|
|
- shiftType
|
|
- requesterExt
|
|
- requesterName
|
|
- targetExt
|
|
- targetName
|
|
- incoming
|
|
properties:
|
|
date:
|
|
type: string
|
|
shiftType:
|
|
type: string
|
|
enum: [day, night]
|
|
requesterExt:
|
|
type: string
|
|
requesterName:
|
|
type: string
|
|
targetExt:
|
|
type: string
|
|
targetName:
|
|
type: string
|
|
incoming:
|
|
type: boolean
|
|
note:
|
|
type: string
|
|
maxLength: 500
|
|
|
|
PendingPickup:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date, shiftType, requesterExt, requesterName, isHoliday]
|
|
properties:
|
|
date:
|
|
type: string
|
|
shiftType:
|
|
type: string
|
|
enum: [day, night]
|
|
requesterExt:
|
|
type: string
|
|
requesterName:
|
|
type: string
|
|
isHoliday:
|
|
type: boolean
|
|
|
|
HolidaySummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [date, label, slots, multiplier]
|
|
properties:
|
|
date:
|
|
type: string
|
|
label:
|
|
type: string
|
|
slots:
|
|
type: integer
|
|
multiplier:
|
|
type: number
|
|
|
|
ShiftsSnapshot:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [linked, isAdmin]
|
|
properties:
|
|
linked:
|
|
type: boolean
|
|
email:
|
|
type: string
|
|
week:
|
|
type: string
|
|
enum: [this, next]
|
|
weekStart:
|
|
type: string
|
|
format: date
|
|
me:
|
|
$ref: "#/components/schemas/RosterPerson"
|
|
isAdmin:
|
|
type: boolean
|
|
days:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ShiftDay"
|
|
pendingSwaps:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PendingSwap"
|
|
pendingPickups:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PendingPickup"
|
|
upcomingHolidays:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/HolidaySummary"
|
|
roster:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/RosterPerson"
|