241 lines
6.8 KiB
GDScript
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))
|