Remove outdated edge gateway object classes, add new agent implementation
Transitioned from obsolete gateway object classes (`edge_gateway_shell_action_jobs_o`, `edge_gateway_shell_events_o`, `edge_gateway_shell_sessions_o`, `edge_gateway_update_jobs_o`) to the new agent implementation (`edge-gateway-agent/agent.php`).
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
|
||||
final class workfeed_shift_time_resolver
|
||||
{
|
||||
private const MAX_UNAPPROVED_EXTENSION_SECONDS = 6 * 3600;
|
||||
private const MAX_UNAPPROVED_SHIFT_SPAN_SECONDS = 24 * 3600;
|
||||
|
||||
/**
|
||||
* @return array{actualStart:DateTime,scheduledEnd:DateTime,actualEnd:DateTime,hasApproval:bool}|null
|
||||
*/
|
||||
public static function resolveShiftTiming(mixed $record_value): ?array
|
||||
{
|
||||
$record = self::normalizeRecord($record_value);
|
||||
if ($record === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$actual_start = self::firstDateTimeFromPaths($record, [
|
||||
'actualStart',
|
||||
'actualStartTime',
|
||||
'clockIn',
|
||||
'clockInTime',
|
||||
'start',
|
||||
'startTime',
|
||||
'from',
|
||||
'approval.originalStart',
|
||||
]);
|
||||
$scheduled_end = self::firstDateTimeFromPaths($record, [
|
||||
'approval.originalEnd',
|
||||
'end',
|
||||
'endTime',
|
||||
'to',
|
||||
]);
|
||||
$actual_only_end = self::firstDateTimeFromPaths($record, [
|
||||
'actualEnd',
|
||||
'actualEndTime',
|
||||
'clockOut',
|
||||
'clockOutTime',
|
||||
]);
|
||||
$current_end = self::firstDateTimeFromPaths($record, [
|
||||
'end',
|
||||
'endTime',
|
||||
'to',
|
||||
]);
|
||||
$saved_actual_end = $actual_only_end ?? $current_end;
|
||||
|
||||
if ($actual_start === null || $scheduled_end === null || $saved_actual_end === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$actual_end = self::resolveActualEnd($record, $actual_start, $scheduled_end, $saved_actual_end, $actual_only_end);
|
||||
if ($actual_end->getTimestamp() <= $actual_start->getTimestamp()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'actualStart' => $actual_start,
|
||||
'scheduledEnd' => $scheduled_end,
|
||||
'actualEnd' => $actual_end,
|
||||
'hasApproval' => self::hasShiftApproval($record),
|
||||
];
|
||||
}
|
||||
|
||||
public static function calculateOvertimeHoursInRange(mixed $record_value, DateTime $range_start, DateTime $range_end_exclusive): float
|
||||
{
|
||||
$timing = self::resolveShiftTiming($record_value);
|
||||
if ($timing === null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$scheduled_end_ts = $timing['scheduledEnd']->getTimestamp();
|
||||
$actual_end_ts = $timing['actualEnd']->getTimestamp();
|
||||
if ($actual_end_ts <= $scheduled_end_ts) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$overtime_start_ts = max($scheduled_end_ts, $range_start->getTimestamp());
|
||||
$overtime_end_ts = min($actual_end_ts, $range_end_exclusive->getTimestamp());
|
||||
if ($overtime_end_ts <= $overtime_start_ts) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round(($overtime_end_ts - $overtime_start_ts) / 3600, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalizeRecord(mixed $record): array
|
||||
{
|
||||
if (is_array($record)) {
|
||||
return $record;
|
||||
}
|
||||
if (is_object($record)) {
|
||||
return get_object_vars($record);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function getNestedRecordValue(array $record, string $path): mixed
|
||||
{
|
||||
$segments = explode('.', $path);
|
||||
$current = $record;
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if (is_array($current)) {
|
||||
if (!array_key_exists($segment, $current)) {
|
||||
return null;
|
||||
}
|
||||
$current = $current[$segment];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object($current)) {
|
||||
if (!property_exists($current, $segment)) {
|
||||
return null;
|
||||
}
|
||||
$current = $current->$segment;
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $paths
|
||||
*/
|
||||
private static function firstDateTimeFromPaths(array $record, array $paths): ?DateTime
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
$parsed = self::parseDateTimeValue(self::getNestedRecordValue($record, $path));
|
||||
if ($parsed !== null) {
|
||||
return $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function parseDateTimeValue(mixed $value): ?DateTime
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$normalized = trim($value);
|
||||
if ($normalized === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new DateTime($normalized);
|
||||
} catch (Exception) {
|
||||
if (!is_numeric($normalized)) {
|
||||
return null;
|
||||
}
|
||||
$value = (float)$normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
if (!is_finite((float)$value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timestamp = (float)$value;
|
||||
if ($timestamp > 9999999999) {
|
||||
$timestamp /= 1000;
|
||||
}
|
||||
|
||||
try {
|
||||
$date = new DateTime('@' . (string)(int)round($timestamp));
|
||||
$date->setTimezone(new DateTimeZone('UTC'));
|
||||
return $date;
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$record = self::normalizeRecord($value);
|
||||
foreach (['seconds', '_seconds', 'epochSeconds', 'timestamp'] as $key) {
|
||||
if (!array_key_exists($key, $record)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed = self::parseDateTimeValue($record[$key]);
|
||||
if ($parsed !== null) {
|
||||
return $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function hasShiftApproval(array $record): bool
|
||||
{
|
||||
if (!array_key_exists('approval', $record)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$approval = $record['approval'];
|
||||
if ($approval === null) {
|
||||
return false;
|
||||
}
|
||||
if (is_array($approval)) {
|
||||
return $approval !== [];
|
||||
}
|
||||
if (is_object($approval)) {
|
||||
return get_object_vars($approval) !== [];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function resolveActualEnd(
|
||||
array $record,
|
||||
DateTime $actual_start,
|
||||
DateTime $scheduled_end,
|
||||
DateTime $saved_actual_end,
|
||||
?DateTime $actual_only_end
|
||||
): DateTime {
|
||||
if (self::hasShiftApproval($record) || $actual_only_end !== null) {
|
||||
return $saved_actual_end;
|
||||
}
|
||||
|
||||
$update_time = self::parseDateTimeValue($record['updateTime'] ?? null);
|
||||
if ($update_time === null) {
|
||||
return $saved_actual_end;
|
||||
}
|
||||
|
||||
$shift_start_ts = $actual_start->getTimestamp();
|
||||
$scheduled_end_ts = $scheduled_end->getTimestamp();
|
||||
$saved_actual_end_ts = $saved_actual_end->getTimestamp();
|
||||
$update_ts = $update_time->getTimestamp();
|
||||
$fallback_base_ts = max($saved_actual_end_ts, $scheduled_end_ts);
|
||||
|
||||
if ($update_ts <= $fallback_base_ts) {
|
||||
return $saved_actual_end;
|
||||
}
|
||||
|
||||
// Only use updateTime as an overtime hint when no explicit actual end was saved.
|
||||
if (($update_ts - $scheduled_end_ts) > self::MAX_UNAPPROVED_EXTENSION_SECONDS) {
|
||||
return $saved_actual_end;
|
||||
}
|
||||
if (($update_ts - $shift_start_ts) > self::MAX_UNAPPROVED_SHIFT_SPAN_SECONDS) {
|
||||
return $saved_actual_end;
|
||||
}
|
||||
|
||||
return $update_time;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user