|
@@ -1441,7 +1441,6 @@ class PersonWorkService extends Service
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$tempRem = (int)$toAllocMin;
|
|
$tempRem = (int)$toAllocMin;
|
|
|
- $firstRowIndex = count($previewList);
|
|
|
|
|
|
|
|
|
|
foreach ($dailyEmpTimePools[$dayTs][$empId] as &$p) {
|
|
foreach ($dailyEmpTimePools[$dayTs][$empId] as &$p) {
|
|
|
if ($tempRem <= 0) break;
|
|
if ($tempRem <= 0) break;
|
|
@@ -1457,28 +1456,30 @@ class PersonWorkService extends Service
|
|
|
|
|
|
|
|
$pMax = (int)($p['e'] - $realStart);
|
|
$pMax = (int)($p['e'] - $realStart);
|
|
|
$take = min($tempRem, $pMax);
|
|
$take = min($tempRem, $pMax);
|
|
|
- // 先尽量按 5 切开;最后余数留给后面回填到本项目某条明细
|
|
|
|
|
|
|
+ // 先尽量按 5 切开;零头必须加到「后面有空档」的同人同项目明细上,避免保存时时间冲突
|
|
|
if ($take >= $step && $tempRem > $step) {
|
|
if ($take >= $step && $tempRem > $step) {
|
|
|
$alignedTake = (int)floor($take / $step) * $step;
|
|
$alignedTake = (int)floor($take / $step) * $step;
|
|
|
- // 若这是最后能放下的量,不要 floor 丢掉属于本项目的零头
|
|
|
|
|
if ($take >= $tempRem) {
|
|
if ($take >= $tempRem) {
|
|
|
- // 整段落完:先取整 5 块,零头下一步加到本条或上一条
|
|
|
|
|
$blocks = (int)floor($tempRem / $step) * $step;
|
|
$blocks = (int)floor($tempRem / $step) * $step;
|
|
|
$odd = $tempRem - $blocks;
|
|
$odd = $tempRem - $blocks;
|
|
|
$take = $blocks > 0 ? $blocks : $tempRem;
|
|
$take = $blocks > 0 ? $blocks : $tempRem;
|
|
|
- // odd 记在 tempRem 里后面统一加回
|
|
|
|
|
if ($blocks > 0 && $odd > 0) {
|
|
if ($blocks > 0 && $odd > 0) {
|
|
|
- // 先写入整 5 块,odd 留在 tempRem
|
|
|
|
|
$realEnd = $realStart + $take;
|
|
$realEnd = $realStart + $take;
|
|
|
|
|
+ // 零头只能加在当前池子剩余空间内,且不与已有明细冲突
|
|
|
|
|
+ $roomInPool = (int)$p['e'] - $realEnd;
|
|
|
|
|
+ $oddToAdd = min($odd, max(0, $roomInPool));
|
|
|
$previewList[] = $this->makePreviewRow(
|
|
$previewList[] = $this->makePreviewRow(
|
|
|
$currentTempMainId, $dayTs, $itemId, $itemTitle, $empId,
|
|
$currentTempMainId, $dayTs, $itemId, $itemTitle, $empId,
|
|
|
$empNameMap[$empId] ?? '未知人员', $realStart, $realEnd, $take
|
|
$empNameMap[$empId] ?? '未知人员', $realStart, $realEnd, $take
|
|
|
);
|
|
);
|
|
|
|
|
+ $rowIdx = count($previewList) - 1;
|
|
|
$tempRem -= $take;
|
|
$tempRem -= $take;
|
|
|
$p['s'] = $realEnd;
|
|
$p['s'] = $realEnd;
|
|
|
- // 把 odd 加到刚写的这条(同人同项目)
|
|
|
|
|
- $this->bumpPreviewRowMinutes($previewList, count($previewList) - 1, $odd);
|
|
|
|
|
- $tempRem -= $odd;
|
|
|
|
|
|
|
+ if ($oddToAdd > 0 && $this->safeBumpPreviewRowMinutes($previewList, $rowIdx, $oddToAdd)) {
|
|
|
|
|
+ $tempRem -= $oddToAdd;
|
|
|
|
|
+ $p['s'] = $realEnd + $oddToAdd;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 加不进去的 odd 留给后面 safe 回填
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
} elseif ($alignedTake > 0) {
|
|
} elseif ($alignedTake > 0) {
|
|
@@ -1497,30 +1498,48 @@ class PersonWorkService extends Service
|
|
|
}
|
|
}
|
|
|
unset($p);
|
|
unset($p);
|
|
|
|
|
|
|
|
- // 池子摆不下或对齐后仍剩的分钟:加到本项目已有明细上(5→6),绝不改到别的项目
|
|
|
|
|
|
|
+ // 剩余分钟:找同人同项目、延长后不冲突的明细往后加
|
|
|
if ($tempRem > 0) {
|
|
if ($tempRem > 0) {
|
|
|
- $bumped = false;
|
|
|
|
|
- for ($i = count($previewList) - 1; $i >= $firstRowIndex; $i--) {
|
|
|
|
|
- if ((int)$previewList[$i]['employee_id'] === (int)$empId
|
|
|
|
|
- && (int)$previewList[$i]['item_id'] === (int)$itemId) {
|
|
|
|
|
- $this->bumpPreviewRowMinutes($previewList, $i, $tempRem);
|
|
|
|
|
- $bumped = true;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $left = $this->safeBumpMinutesOntoEmpItem(
|
|
|
|
|
+ $previewList,
|
|
|
|
|
+ (int)$empId,
|
|
|
|
|
+ (int)$itemId,
|
|
|
|
|
+ $tempRem,
|
|
|
|
|
+ date('Y-m-d', $dayTs)
|
|
|
|
|
+ );
|
|
|
|
|
+ if ($left > 0) {
|
|
|
|
|
+ // 当天加不下,再在其它日期的同人同项目明细上找空档
|
|
|
|
|
+ $left = $this->safeBumpMinutesOntoEmpItem(
|
|
|
|
|
+ $previewList,
|
|
|
|
|
+ (int)$empId,
|
|
|
|
|
+ (int)$itemId,
|
|
|
|
|
+ $left,
|
|
|
|
|
+ null
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
- if (!$bumped) {
|
|
|
|
|
- // 极端:当天还没有任何明细,造一条 0 起点的余数行
|
|
|
|
|
- $previewList[] = $this->makePreviewRow(
|
|
|
|
|
- $currentTempMainId, $dayTs, $itemId, $itemTitle, $empId,
|
|
|
|
|
- $empNameMap[$empId] ?? '未知人员', 0, $tempRem, $tempRem
|
|
|
|
|
|
|
+ // 仍剩:在当天找同人空档新开一条短明细(仍属本项目)
|
|
|
|
|
+ if ($left > 0) {
|
|
|
|
|
+ $placed = $this->insertRemainderRowInFreeGap(
|
|
|
|
|
+ $previewList,
|
|
|
|
|
+ $currentTempMainId,
|
|
|
|
|
+ $dayTs,
|
|
|
|
|
+ $itemId,
|
|
|
|
|
+ $itemTitle,
|
|
|
|
|
+ $empId,
|
|
|
|
|
+ $empNameMap[$empId] ?? '未知人员',
|
|
|
|
|
+ $left
|
|
|
);
|
|
);
|
|
|
|
|
+ if ($placed) {
|
|
|
|
|
+ $left = 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ $tempRem = $left;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3.1 最终校验:每人每项目合计必须等于月目标,差一分钟就加回该人该项目某条明细
|
|
|
|
|
|
|
+ // 3.1 最终校验:每人每项目合计必须等于月目标;差额安全加回(不制造重叠)
|
|
|
$previewList = $this->reconcilePreviewToProjectTargets($previewList, $empProjectTargets);
|
|
$previewList = $this->reconcilePreviewToProjectTargets($previewList, $empProjectTargets);
|
|
|
|
|
|
|
|
return ['status' => true, 'data' => $previewList];
|
|
return ['status' => true, 'data' => $previewList];
|
|
@@ -1546,7 +1565,6 @@ class PersonWorkService extends Service
|
|
|
return [];
|
|
return [];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 比例按配置百分比;若合计不是 100,按相对权重缩放,避免丢分钟
|
|
|
|
|
$targets = [];
|
|
$targets = [];
|
|
|
$parts = [];
|
|
$parts = [];
|
|
|
$used = 0;
|
|
$used = 0;
|
|
@@ -1598,31 +1616,252 @@ class PersonWorkService extends Service
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function rowStartMin(array $row)
|
|
|
|
|
+ {
|
|
|
|
|
+ return ((int)$row['start_hour']) * 60 + (int)$row['start_min'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function rowEndMin(array $row)
|
|
|
|
|
+ {
|
|
|
|
|
+ return ((int)$row['end_hour']) * 60 + (int)$row['end_min'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 把零头分钟加到指定明细:延长 end,total_work_min+n(例如 5→6)
|
|
|
|
|
|
|
+ * 指定下标延长 end,不校验冲突(调用方需先确保安全)
|
|
|
*/
|
|
*/
|
|
|
private function bumpPreviewRowMinutes(array &$previewList, $index, $addMin)
|
|
private function bumpPreviewRowMinutes(array &$previewList, $index, $addMin)
|
|
|
{
|
|
{
|
|
|
- if ($addMin <= 0 || !isset($previewList[$index])) {
|
|
|
|
|
|
|
+ if ($addMin == 0 || !isset($previewList[$index])) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
$row = &$previewList[$index];
|
|
$row = &$previewList[$index];
|
|
|
- $start = ((int)$row['start_hour']) * 60 + (int)$row['start_min'];
|
|
|
|
|
- $end = ((int)$row['end_hour']) * 60 + (int)$row['end_min'];
|
|
|
|
|
- $end += (int)$addMin;
|
|
|
|
|
|
|
+ $end = $this->rowEndMin($row) + (int)$addMin;
|
|
|
|
|
+ if ($end < $this->rowStartMin($row)) {
|
|
|
|
|
+ $end = $this->rowStartMin($row);
|
|
|
|
|
+ }
|
|
|
$row['end_hour'] = (int)floor($end / 60);
|
|
$row['end_hour'] = (int)floor($end / 60);
|
|
|
$row['end_min'] = (int)($end % 60);
|
|
$row['end_min'] = (int)($end % 60);
|
|
|
$row['end_time'] = sprintf('%02d:%02d', $row['end_hour'], $row['end_min']);
|
|
$row['end_time'] = sprintf('%02d:%02d', $row['end_hour'], $row['end_min']);
|
|
|
- $row['total_work_min'] = ((int)$row['total_work_min']) + (int)$addMin;
|
|
|
|
|
- // 防止异常倒挂
|
|
|
|
|
- if ($end < $start) {
|
|
|
|
|
- $row['total_work_min'] = max(0, $end - $start);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $row['total_work_min'] = max(0, $end - $this->rowStartMin($row));
|
|
|
unset($row);
|
|
unset($row);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 最终对齐:每人每项目明细合计必须等于月目标;差额加到该人该项目任意一条明细
|
|
|
|
|
|
|
+ * 判断把 index 行结束时间延长 addMin 后,是否与同人同日其它明细冲突
|
|
|
|
|
+ */
|
|
|
|
|
+ private function canBumpRowWithoutOverlap(array $previewList, $index, $addMin)
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($addMin <= 0 || !isset($previewList[$index])) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ $row = $previewList[$index];
|
|
|
|
|
+ $empId = (int)$row['employee_id'];
|
|
|
|
|
+ $dateStr = $row['order_time'];
|
|
|
|
|
+ $newS = $this->rowStartMin($row);
|
|
|
|
|
+ $newE = $this->rowEndMin($row) + (int)$addMin;
|
|
|
|
|
+ if ($newE <= $newS) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($previewList as $i => $other) {
|
|
|
|
|
+ if ($i === $index) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ((int)$other['employee_id'] !== $empId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($other['order_time'] !== $dateStr) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $os = $this->rowStartMin($other);
|
|
|
|
|
+ $oe = $this->rowEndMin($other);
|
|
|
|
|
+ if ($newS < $oe && $newE > $os) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全延长指定行(通过冲突检查才改)
|
|
|
|
|
+ */
|
|
|
|
|
+ private function safeBumpPreviewRowMinutes(array &$previewList, $index, $addMin)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!$this->canBumpRowWithoutOverlap($previewList, $index, $addMin)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ $this->bumpPreviewRowMinutes($previewList, $index, $addMin);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 把分钟安全加到「同人 + 同项目」某条明细后面(优先指定日期)
|
|
|
|
|
+ * @return int 未能加上的剩余分钟
|
|
|
|
|
+ */
|
|
|
|
|
+ private function safeBumpMinutesOntoEmpItem(array &$previewList, $empId, $itemId, $addMin, $preferDate = null)
|
|
|
|
|
+ {
|
|
|
|
|
+ $addMin = (int)$addMin;
|
|
|
|
|
+ if ($addMin <= 0) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 候选:同人同项目,按「结束后空档大小」优先;同日优先
|
|
|
|
|
+ $candidates = [];
|
|
|
|
|
+ foreach ($previewList as $i => $row) {
|
|
|
|
|
+ if ((int)$row['employee_id'] !== (int)$empId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ((int)$row['item_id'] !== (int)$itemId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($preferDate !== null && $row['order_time'] !== $preferDate) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $gap = $this->gapAfterRow($previewList, $i);
|
|
|
|
|
+ if ($gap <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $candidates[] = [
|
|
|
|
|
+ 'index' => $i,
|
|
|
|
|
+ 'gap' => $gap,
|
|
|
|
|
+ 'end' => $this->rowEndMin($row),
|
|
|
|
|
+ 'same_day' => 1,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 指定日没有空档时,扩大到其它日期
|
|
|
|
|
+ if (empty($candidates) && $preferDate !== null) {
|
|
|
|
|
+ return $this->safeBumpMinutesOntoEmpItem($previewList, $empId, $itemId, $addMin, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ usort($candidates, function ($a, $b) {
|
|
|
|
|
+ if ($a['gap'] == $b['gap']) {
|
|
|
|
|
+ return $b['end'] <=> $a['end'];
|
|
|
|
|
+ }
|
|
|
|
|
+ return $b['gap'] <=> $a['gap'];
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ $left = $addMin;
|
|
|
|
|
+ foreach ($candidates as $c) {
|
|
|
|
|
+ if ($left <= 0) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ $give = min($left, $c['gap']);
|
|
|
|
|
+ // 再确认一次(可能前面已经 bump 过改变了空隙)
|
|
|
|
|
+ $gapNow = $this->gapAfterRow($previewList, $c['index']);
|
|
|
|
|
+ $give = min($give, $gapNow);
|
|
|
|
|
+ if ($give <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($this->safeBumpPreviewRowMinutes($previewList, $c['index'], $give)) {
|
|
|
|
|
+ $left -= $give;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return max(0, $left);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 某条明细结束后,到同人同日下一段开始之间的空档分钟数
|
|
|
|
|
+ */
|
|
|
|
|
+ private function gapAfterRow(array $previewList, $index)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!isset($previewList[$index])) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ $row = $previewList[$index];
|
|
|
|
|
+ $empId = (int)$row['employee_id'];
|
|
|
|
|
+ $dateStr = $row['order_time'];
|
|
|
|
|
+ $end = $this->rowEndMin($row);
|
|
|
|
|
+
|
|
|
|
|
+ $nextStart = null;
|
|
|
|
|
+ foreach ($previewList as $i => $other) {
|
|
|
|
|
+ if ($i === $index) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ((int)$other['employee_id'] !== $empId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($other['order_time'] !== $dateStr) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $os = $this->rowStartMin($other);
|
|
|
|
|
+ if ($os >= $end) {
|
|
|
|
|
+ if ($nextStart === null || $os < $nextStart) {
|
|
|
|
|
+ $nextStart = $os;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 没有下一段:最多延到当天 24:00,避免无限延长
|
|
|
|
|
+ if ($nextStart === null) {
|
|
|
|
|
+ $nextStart = 24 * 60;
|
|
|
|
|
+ }
|
|
|
|
|
+ return max(0, $nextStart - $end);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在同人当天已有时间线的空隙中,插入一条余数明细(仍属本项目)
|
|
|
|
|
+ */
|
|
|
|
|
+ private function insertRemainderRowInFreeGap(
|
|
|
|
|
+ array &$previewList,
|
|
|
|
|
+ $tempMainId,
|
|
|
|
|
+ $dayTs,
|
|
|
|
|
+ $itemId,
|
|
|
|
|
+ $itemTitle,
|
|
|
|
|
+ $empId,
|
|
|
|
|
+ $empTitle,
|
|
|
|
|
+ $needMin
|
|
|
|
|
+ ) {
|
|
|
|
|
+ $needMin = (int)$needMin;
|
|
|
|
|
+ if ($needMin <= 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ $dateStr = date('Y-m-d', $dayTs);
|
|
|
|
|
+
|
|
|
|
|
+ // 收集当天该人所有区间
|
|
|
|
|
+ $intervals = [];
|
|
|
|
|
+ foreach ($previewList as $row) {
|
|
|
|
|
+ if ((int)$row['employee_id'] !== (int)$empId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($row['order_time'] !== $dateStr) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $intervals[] = ['s' => $this->rowStartMin($row), 'e' => $this->rowEndMin($row)];
|
|
|
|
|
+ }
|
|
|
|
|
+ usort($intervals, function ($a, $b) {
|
|
|
|
|
+ return $a['s'] <=> $b['s'];
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 合并后找空隙
|
|
|
|
|
+ $cursor = 0;
|
|
|
|
|
+ $dayEnd = 24 * 60;
|
|
|
|
|
+ foreach ($intervals as $iv) {
|
|
|
|
|
+ if ($iv['s'] > $cursor) {
|
|
|
|
|
+ $gap = $iv['s'] - $cursor;
|
|
|
|
|
+ if ($gap >= $needMin) {
|
|
|
|
|
+ $previewList[] = $this->makePreviewRow(
|
|
|
|
|
+ $tempMainId, $dayTs, $itemId, $itemTitle, $empId, $empTitle,
|
|
|
|
|
+ $cursor, $cursor + $needMin, $needMin
|
|
|
|
|
+ );
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $cursor = max($cursor, $iv['e']);
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($dayEnd - $cursor >= $needMin) {
|
|
|
|
|
+ $previewList[] = $this->makePreviewRow(
|
|
|
|
|
+ $tempMainId, $dayTs, $itemId, $itemTitle, $empId, $empTitle,
|
|
|
|
|
+ $cursor, $cursor + $needMin, $needMin
|
|
|
|
|
+ );
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 最终对齐:每人每项目明细合计必须等于月目标;差额安全加回(不制造重叠)
|
|
|
*/
|
|
*/
|
|
|
private function reconcilePreviewToProjectTargets(array $previewList, array $empProjectTargets)
|
|
private function reconcilePreviewToProjectTargets(array $previewList, array $empProjectTargets)
|
|
|
{
|
|
{
|
|
@@ -1631,12 +1870,10 @@ class PersonWorkService extends Service
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$actual = [];
|
|
$actual = [];
|
|
|
- $lastIndex = [];
|
|
|
|
|
- foreach ($previewList as $i => $row) {
|
|
|
|
|
|
|
+ foreach ($previewList as $row) {
|
|
|
$empId = (int)$row['employee_id'];
|
|
$empId = (int)$row['employee_id'];
|
|
|
$itemId = (int)$row['item_id'];
|
|
$itemId = (int)$row['item_id'];
|
|
|
$actual[$empId][$itemId] = ($actual[$empId][$itemId] ?? 0) + (int)$row['total_work_min'];
|
|
$actual[$empId][$itemId] = ($actual[$empId][$itemId] ?? 0) + (int)$row['total_work_min'];
|
|
|
- $lastIndex[$empId][$itemId] = $i;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
foreach ($empProjectTargets as $empId => $items) {
|
|
foreach ($empProjectTargets as $empId => $items) {
|
|
@@ -1646,15 +1883,86 @@ class PersonWorkService extends Service
|
|
|
if ($diff === 0) {
|
|
if ($diff === 0) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- if ($diff > 0 && isset($lastIndex[$empId][$itemId])) {
|
|
|
|
|
- // 少了:加到该人该项目最后一条(5 变 6…)
|
|
|
|
|
- $this->bumpPreviewRowMinutes($previewList, $lastIndex[$empId][$itemId], $diff);
|
|
|
|
|
- } elseif ($diff < 0 && isset($lastIndex[$empId][$itemId])) {
|
|
|
|
|
- // 多了:从最后一条扣(极端情况)
|
|
|
|
|
- $idx = $lastIndex[$empId][$itemId];
|
|
|
|
|
- $canCut = min(-$diff, (int)$previewList[$idx]['total_work_min']);
|
|
|
|
|
- if ($canCut > 0) {
|
|
|
|
|
- $this->bumpPreviewRowMinutes($previewList, $idx, -$canCut);
|
|
|
|
|
|
|
+ if ($diff > 0) {
|
|
|
|
|
+ $left = $this->safeBumpMinutesOntoEmpItem($previewList, $empId, $itemId, $diff, null);
|
|
|
|
|
+ // 仍缺:按天找空档插入
|
|
|
|
|
+ if ($left > 0) {
|
|
|
|
|
+ $dates = [];
|
|
|
|
|
+ foreach ($previewList as $row) {
|
|
|
|
|
+ if ((int)$row['employee_id'] === (int)$empId) {
|
|
|
|
|
+ $dates[$row['order_time']] = $row['order_timestamp'] ?? strtotime($row['order_time']);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ($dates as $dateStr => $ts) {
|
|
|
|
|
+ if ($left <= 0) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ $itemTitle = '';
|
|
|
|
|
+ foreach ($previewList as $row) {
|
|
|
|
|
+ if ((int)$row['employee_id'] === (int)$empId && (int)$row['item_id'] === (int)$itemId) {
|
|
|
|
|
+ $itemTitle = $row['item_title'];
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $empTitle = '';
|
|
|
|
|
+ foreach ($previewList as $row) {
|
|
|
|
|
+ if ((int)$row['employee_id'] === (int)$empId) {
|
|
|
|
|
+ $empTitle = $row['employee_title'];
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $before = $left;
|
|
|
|
|
+ if ($this->insertRemainderRowInFreeGap(
|
|
|
|
|
+ $previewList,
|
|
|
|
|
+ 'reconcile_' . $empId . '_' . $itemId,
|
|
|
|
|
+ (int)$ts,
|
|
|
|
|
+ $itemId,
|
|
|
|
|
+ $itemTitle ?: '未知项目',
|
|
|
|
|
+ $empId,
|
|
|
|
|
+ $empTitle ?: '未知人员',
|
|
|
|
|
+ $left
|
|
|
|
|
+ )) {
|
|
|
|
|
+ $left = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 插不进整天 needMin,尝试拆小:逐分钟插太慢;改为能插多少插多少
|
|
|
|
|
+ if ($left > 0) {
|
|
|
|
|
+ for ($n = $left; $n >= 1; $n--) {
|
|
|
|
|
+ if ($this->insertRemainderRowInFreeGap(
|
|
|
|
|
+ $previewList,
|
|
|
|
|
+ 'reconcile_' . $empId . '_' . $itemId,
|
|
|
|
|
+ (int)$ts,
|
|
|
|
|
+ $itemId,
|
|
|
|
|
+ $itemTitle ?: '未知项目',
|
|
|
|
|
+ $empId,
|
|
|
|
|
+ $empTitle ?: '未知人员',
|
|
|
|
|
+ $n
|
|
|
|
|
+ )) {
|
|
|
|
|
+ $left -= $n;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ unset($before);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } elseif ($diff < 0) {
|
|
|
|
|
+ // 多了:从有空可缩的明细尾部扣(不制造 start>=end)
|
|
|
|
|
+ $needCut = -$diff;
|
|
|
|
|
+ for ($i = count($previewList) - 1; $i >= 0 && $needCut > 0; $i--) {
|
|
|
|
|
+ $row = $previewList[$i];
|
|
|
|
|
+ if ((int)$row['employee_id'] !== (int)$empId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ((int)$row['item_id'] !== (int)$itemId) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $len = (int)$row['total_work_min'];
|
|
|
|
|
+ if ($len <= 1) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $cut = min($needCut, $len - 1);
|
|
|
|
|
+ $this->bumpPreviewRowMinutes($previewList, $i, -$cut);
|
|
|
|
|
+ $needCut -= $cut;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1666,7 +1974,7 @@ class PersonWorkService extends Service
|
|
|
/**
|
|
/**
|
|
|
* 按各项目剩余月目标,把当天额度拆开(最大余数法)
|
|
* 按各项目剩余月目标,把当天额度拆开(最大余数法)
|
|
|
* - 非最后一天:尽量按 5 分钟块分配
|
|
* - 非最后一天:尽量按 5 分钟块分配
|
|
|
- * - 当天 1~4 分钟零头:补给「仍有剩余目标」的项目自身,不挪到无关项目逻辑外的“末项”
|
|
|
|
|
|
|
+ * - 当天 1~4 分钟零头:补给「仍有剩余目标」的项目自身
|
|
|
*
|
|
*
|
|
|
* @param int $dayBudget 当天可分配总分钟
|
|
* @param int $dayBudget 当天可分配总分钟
|
|
|
* @param array $projRem [item_id => 剩余目标分钟]
|
|
* @param array $projRem [item_id => 剩余目标分钟]
|