Files
TheTree_mock/scripts/staff/shift_instance.gd
T

241 lines
6.8 KiB
GDScript

class_name ShiftInstance
extends RefCounted
## One concrete shift, identified by its start day even when it ends on the
## following day. Absolute integer minutes avoid every midnight ambiguity.
enum Kind {
DAY,
NIGHT,
FLEX,
}
const MINUTES_PER_DAY := 1_440
const COMMUTE_LEAD_MINUTES := 30
const DAY_START := 360
const DAY_END := 1_080
const NIGHT_START := 1_080
const NIGHT_END := 360
var shift_instance_id: StringName = &""
var staff_id: StringName = &""
var template_id: StringName = &"FLEX"
var kind: Kind = Kind.FLEX
var start_day := 0
var start_minute_of_day := 0
var end_minute_of_day := 1
var start_at := 0
var end_at := 1
var assigned_facility_id: StringName = &""
var cancelled := false
var actual_started_at := -1
var actual_ended_at := -1
var worked_minutes := 0
var overtime_minutes := 0
var late_minutes := 0
var late_reason: StringName = &""
func configure(
p_shift_instance_id: StringName,
p_staff_id: StringName,
p_template_id: StringName,
p_start_day: int,
p_start_minute_of_day: int,
p_end_minute_of_day: int,
p_assigned_facility_id: StringName = &"",
options: Dictionary = {}
) -> ShiftInstance:
shift_instance_id = p_shift_instance_id
staff_id = p_staff_id
template_id = p_template_id if not p_template_id.is_empty() else &"FLEX"
kind = _kind_from_template(template_id)
start_day = maxi(p_start_day, 0)
start_minute_of_day = p_start_minute_of_day
end_minute_of_day = p_end_minute_of_day
start_at = start_day * MINUTES_PER_DAY + start_minute_of_day
end_at = start_day * MINUTES_PER_DAY + end_minute_of_day
if end_minute_of_day <= start_minute_of_day:
end_at += MINUTES_PER_DAY
assigned_facility_id = p_assigned_facility_id
cancelled = bool(_value(options, &"cancelled", false))
actual_started_at = int(_value(options, &"actual_started_at", -1))
actual_ended_at = int(_value(options, &"actual_ended_at", -1))
worked_minutes = maxi(int(_value(options, &"worked_minutes", 0)), 0)
overtime_minutes = maxi(int(_value(options, &"overtime_minutes", 0)), 0)
late_minutes = maxi(int(_value(options, &"late_minutes", 0)), 0)
late_reason = StringName(_value(options, &"late_reason", &""))
return self
static func create_standard(
p_shift_instance_id: StringName,
p_staff_id: StringName,
p_template_id: StringName,
p_start_day: int,
p_assigned_facility_id: StringName = &""
) -> ShiftInstance:
var normalized := String(p_template_id).to_upper()
if normalized == "DAY":
return ShiftInstance.new().configure(
p_shift_instance_id,
p_staff_id,
&"DAY",
p_start_day,
DAY_START,
DAY_END,
p_assigned_facility_id
)
if normalized == "NIGHT":
return ShiftInstance.new().configure(
p_shift_instance_id,
p_staff_id,
&"NIGHT",
p_start_day,
NIGHT_START,
NIGHT_END,
p_assigned_facility_id
)
return null
func is_valid() -> bool:
return (
not shift_instance_id.is_empty()
and not staff_id.is_empty()
and start_day >= 0
and start_minute_of_day >= 0
and start_minute_of_day < MINUTES_PER_DAY
and end_minute_of_day >= 0
and end_minute_of_day < MINUTES_PER_DAY
and end_at > start_at
and end_at - start_at <= MINUTES_PER_DAY
and worked_minutes >= 0
and overtime_minutes >= 0
and late_minutes >= 0
)
func crosses_midnight() -> bool:
return end_at / MINUTES_PER_DAY > start_at / MINUTES_PER_DAY
func contains_minute(absolute_minute: int) -> bool:
return not cancelled and absolute_minute >= start_at and absolute_minute < end_at
func is_commute_window(absolute_minute: int) -> bool:
return (
not cancelled
and absolute_minute >= start_at - COMMUTE_LEAD_MINUTES
and absolute_minute < start_at
)
func has_ended(absolute_minute: int) -> bool:
return cancelled or absolute_minute >= end_at
func record_duty_minute(absolute_minute: int, overtime := false) -> bool:
if actual_started_at < 0:
actual_started_at = absolute_minute
if overtime:
if absolute_minute < end_at:
return false
overtime_minutes += 1
return true
if not contains_minute(absolute_minute):
return false
worked_minutes += 1
return true
func close(absolute_minute: int) -> void:
if actual_ended_at < 0:
actual_ended_at = maxi(absolute_minute, start_at)
func mark_late(absolute_minute: int, reason: StringName) -> bool:
if absolute_minute < start_at + 15:
return false
late_minutes = maxi(absolute_minute - start_at, 15)
late_reason = reason
return true
func create_snapshot() -> Dictionary:
return {
&"shift_instance_id": String(shift_instance_id),
&"staff_id": String(staff_id),
&"template_id": String(template_id),
&"kind": Kind.keys()[kind],
&"start_day": start_day,
&"start_minute_of_day": start_minute_of_day,
&"end_minute_of_day": end_minute_of_day,
&"start_at": start_at,
&"end_at": end_at,
&"assigned_facility_id": String(assigned_facility_id),
&"cancelled": cancelled,
&"actual_started_at": actual_started_at,
&"actual_ended_at": actual_ended_at,
&"worked_minutes": worked_minutes,
&"overtime_minutes": overtime_minutes,
&"late_minutes": late_minutes,
&"late_reason": String(late_reason),
}
static func from_snapshot(snapshot: Dictionary) -> ShiftInstance:
if not _snapshot_shape_is_valid(snapshot):
return null
var shift := ShiftInstance.new().configure(
StringName(snapshot.get(&"shift_instance_id", "")),
StringName(snapshot.get(&"staff_id", "")),
StringName(snapshot.get(&"template_id", "FLEX")),
int(snapshot.get(&"start_day", 0)),
int(snapshot.get(&"start_minute_of_day", 0)),
int(snapshot.get(&"end_minute_of_day", 1)),
StringName(snapshot.get(&"assigned_facility_id", "")),
{
&"cancelled": snapshot.get(&"cancelled", false),
&"actual_started_at": snapshot.get(&"actual_started_at", -1),
&"actual_ended_at": snapshot.get(&"actual_ended_at", -1),
&"worked_minutes": snapshot.get(&"worked_minutes", 0),
&"overtime_minutes": snapshot.get(&"overtime_minutes", 0),
&"late_minutes": snapshot.get(&"late_minutes", 0),
&"late_reason": snapshot.get(&"late_reason", ""),
}
)
if shift.start_at != int(snapshot.get(&"start_at", -1)):
return null
if shift.end_at != int(snapshot.get(&"end_at", -1)):
return null
var kind_index := Kind.keys().find(String(snapshot.get(&"kind", "")))
if kind_index < 0 or shift.kind != kind_index:
return null
return shift if shift.is_valid() else null
static func _snapshot_shape_is_valid(snapshot: Dictionary) -> bool:
return (
not String(snapshot.get(&"shift_instance_id", "")).is_empty()
and not String(snapshot.get(&"staff_id", "")).is_empty()
and int(snapshot.get(&"start_day", -1)) >= 0
and int(snapshot.get(&"worked_minutes", -1)) >= 0
and int(snapshot.get(&"overtime_minutes", -1)) >= 0
)
static func _kind_from_template(value: StringName) -> Kind:
match String(value).to_upper():
"DAY":
return Kind.DAY
"NIGHT":
return Kind.NIGHT
_:
return Kind.FLEX
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
return source.get(key, source.get(String(key), fallback))