|
@@ -1054,7 +1054,7 @@ class DeviceWorkService extends Service
|
|
|
]];
|
|
]];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function calculateDailyDeviceAllocation($monthStart, $topDepartId, $user)
|
|
|
|
|
|
|
+ private function calculateDailyDeviceAllocation1($monthStart, $topDepartId, $user)
|
|
|
{
|
|
{
|
|
|
// 加载月度设备明细
|
|
// 加载月度设备明细
|
|
|
$monthlyOrder = DB::table('monthly_dw_order_details as d')
|
|
$monthlyOrder = DB::table('monthly_dw_order_details as d')
|
|
@@ -1220,6 +1220,242 @@ class DeviceWorkService extends Service
|
|
|
return ['status' => true, 'data' => $previewList];
|
|
return ['status' => true, 'data' => $previewList];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function calculateDailyDeviceAllocation($monthStart, $topDepartId, $user)
|
|
|
|
|
+ {
|
|
|
|
|
+ // 1. 加载月度设备明细
|
|
|
|
|
+ $monthlyOrder = DB::table('monthly_dw_order_details as d')
|
|
|
|
|
+ ->join('monthly_dw_order as m', 'm.id', '=', 'd.main_id')
|
|
|
|
|
+ ->where('m.month', $monthStart)
|
|
|
|
|
+ ->where('m.top_depart_id', $topDepartId)
|
|
|
|
|
+ ->where('m.del_time', 0)
|
|
|
|
|
+ ->where('d.del_time', 0)
|
|
|
|
|
+ ->select('d.*')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ if ($monthlyOrder->isEmpty()) return ['status' => false, 'msg' => '未找到设备月度工时明细'];
|
|
|
|
|
+
|
|
|
|
|
+ $usedDeviceIds = $monthlyOrder->pluck('device_id')->unique()->toArray();
|
|
|
|
|
+
|
|
|
|
|
+ $deviceMap = DB::table('device')
|
|
|
|
|
+ ->whereIn('id', $usedDeviceIds)
|
|
|
|
|
+ ->pluck('title', 'id')
|
|
|
|
|
+ ->toArray();
|
|
|
|
|
+
|
|
|
|
|
+ // 加载分配规则
|
|
|
|
|
+ $ruleSet = DB::table('rule_set_details as rd')
|
|
|
|
|
+ ->join('rule_set as r', 'r.id', '=', 'rd.main_id')
|
|
|
|
|
+ ->where('r.month', $monthStart)
|
|
|
|
|
+ ->where('rd.type', 2) // 设备类型
|
|
|
|
|
+ ->where('rd.top_depart_id', $topDepartId)
|
|
|
|
|
+ ->where('r.del_time', 0)
|
|
|
|
|
+ ->where('rd.del_time', 0)
|
|
|
|
|
+ ->select('rd.*')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ $usedItemIds = $ruleSet->pluck('item_id')->unique()->toArray();
|
|
|
|
|
+ $itemMap = DB::table('item')
|
|
|
|
|
+ ->whereIn('id', $usedItemIds)
|
|
|
|
|
+ ->pluck('title', 'id')
|
|
|
|
|
+ ->toArray();
|
|
|
|
|
+
|
|
|
|
|
+ $ruleSetGrouped = $ruleSet->groupBy('data_id');
|
|
|
|
|
+
|
|
|
|
|
+ // 标准班次 & 日历
|
|
|
|
|
+ $standardWorkRanges = DB::table('work_range_details')
|
|
|
|
|
+ ->where('top_depart_id', $topDepartId)
|
|
|
|
|
+ ->where('del_time', 0)
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ // 【关键改动】让标准班次的可用时间,天然就是30分钟的整数倍(向下取整,确保格子规整)
|
|
|
|
|
+ $step = 30;
|
|
|
|
|
+ $dayMaxAvail = 0;
|
|
|
|
|
+ $cleanWorkRanges = [];
|
|
|
|
|
+ foreach ($standardWorkRanges as $swr) {
|
|
|
|
|
+ $s = (int)($swr->start_time_hour * 60 + $swr->start_time_min);
|
|
|
|
|
+ $e = (int)($swr->end_time_hour * 60 + $swr->end_time_min);
|
|
|
|
|
+ // 起始点对齐
|
|
|
|
|
+ if ($s % $step != 0) $s = (int)ceil($s / $step) * $step;
|
|
|
|
|
+ if ($e % $step != 0) $e = (int)floor($e / $step) * $step;
|
|
|
|
|
+
|
|
|
|
|
+ if ($e > $s) {
|
|
|
|
|
+ $cleanWorkRanges[] = ['s' => $s, 'e' => $e];
|
|
|
|
|
+ $dayMaxAvail += ($e - $s);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $workDays = DB::table('calendar_details')
|
|
|
|
|
+ ->where('top_depart_id', $topDepartId)
|
|
|
|
|
+ ->where('month', $monthStart)
|
|
|
|
|
+ ->where('is_work', 1)
|
|
|
|
|
+ ->where('del_time', 0)
|
|
|
|
|
+ ->orderBy('time', 'asc')->get();
|
|
|
|
|
+
|
|
|
|
|
+ if ($workDays->isEmpty()) return ['status' => false, 'msg' => '未配置工作日历'];
|
|
|
|
|
+
|
|
|
|
|
+ // --- 2. 阶段一:计算每天每台设备分配的项目分钟数 (加入30分钟对齐无损控制) ---
|
|
|
|
|
+ $finalAlloc = [];
|
|
|
|
|
+ foreach ($monthlyOrder as $mDetail) {
|
|
|
|
|
+ $deviceId = $mDetail->device_id;
|
|
|
|
|
+ $deviceRules = $ruleSetGrouped->get($deviceId);
|
|
|
|
|
+ if (!$deviceRules) continue;
|
|
|
|
|
+
|
|
|
|
|
+ $remainingMin = (int)round((float)$mDetail->rd_total_hours * 60);
|
|
|
|
|
+ if ($remainingMin <= 0) continue;
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($workDays as $dayInfo) {
|
|
|
|
|
+ if ($remainingMin <= 0) break;
|
|
|
|
|
+
|
|
|
|
|
+ // 当天最大可分,必须是30的倍数(最后一天除外,最后一天要把零头全部收走)
|
|
|
|
|
+ $canAllocToday = min($remainingMin, $dayMaxAvail);
|
|
|
|
|
+ // 确保非最后一天的日额度也是30的倍数
|
|
|
|
|
+ if ($remainingMin > $dayMaxAvail && $canAllocToday % $step != 0) {
|
|
|
|
|
+ $canAllocToday = (int)floor($canAllocToday / $step) * $step;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $allocatedInDay = 0;
|
|
|
|
|
+ $ruleCount = count($deviceRules);
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($deviceRules as $index => $rule) {
|
|
|
|
|
+ if ($index === $ruleCount - 1) {
|
|
|
|
|
+ // 最后一个项目拿走今天剩下的所有分钟
|
|
|
|
|
+ $projectMin = $canAllocToday - $allocatedInDay;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $rate = (float)$rule->rate / 100;
|
|
|
|
|
+ // 【优化】计算出来的项目时间,直接四舍五入到最近的30分钟整数倍
|
|
|
|
|
+ $rawMin = $canAllocToday * $rate;
|
|
|
|
|
+ $projectMin = (int)round($rawMin / $step) * $step;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 防止四舍五入爆池子
|
|
|
|
|
+ if ($allocatedInDay + $projectMin > $canAllocToday) {
|
|
|
|
|
+ $projectMin = $canAllocToday - $allocatedInDay;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($projectMin > 0) {
|
|
|
|
|
+ $finalAlloc[$dayInfo->time][$rule->item_id][$deviceId] = $projectMin;
|
|
|
|
|
+ $allocatedInDay += $projectMin;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $remainingMin -= $allocatedInDay; // 减去实际扣减的,确保闭环
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 【大招:溢出工时补偿】如果日历天数走完了,因为设备加班导致 remainingMin 还有剩
|
|
|
|
|
+ // 将剩下的零头,强行追加分配到有空间的最后几天里,确保总工时 168 小时一分不差
|
|
|
|
|
+ if ($remainingMin > 0) {
|
|
|
|
|
+ foreach (array_reverse($workDays->toArray()) as $dayInfo) {
|
|
|
|
|
+ if ($remainingMin <= 0) break;
|
|
|
|
|
+ // 找到该设备当天已分配的总量
|
|
|
|
|
+ $todayAllocated = 0;
|
|
|
|
|
+ foreach ($finalAlloc[$dayInfo->time] ?? [] as $itemId => $devs) {
|
|
|
|
|
+ $todayAllocated += $devs[$deviceId] ?? 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 哪怕超过标准班次,为了不丢工时,硬塞进去(这部分会作为加班在后续无缝拉长)
|
|
|
|
|
+ $lastRule = $deviceRules->last();
|
|
|
|
|
+ if ($lastRule) {
|
|
|
|
|
+ $finalAlloc[$dayInfo->time][$lastRule->item_id][$deviceId] =
|
|
|
|
|
+ ($finalAlloc[$dayInfo->time][$lastRule->item_id][$deviceId] ?? 0) + $remainingMin;
|
|
|
|
|
+ $remainingMin = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // --- 3. 阶段二:生成预览行 (采用无缝滑行指针,杜绝截断丢弃) ---
|
|
|
|
|
+ $previewList = [];
|
|
|
|
|
+ $dailyDevicePools = [];
|
|
|
|
|
+ $tempMainIdCounter = 1;
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($finalAlloc as $dayTs => $projects) {
|
|
|
|
|
+ foreach ($projects as $itemId => $devices) {
|
|
|
|
|
+
|
|
|
|
|
+ $currentTempMainId = $tempMainIdCounter++;
|
|
|
|
|
+ $itemTitle = $itemMap[$itemId] ?? '未知项目';
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($devices as $deviceId => $toAllocMin) {
|
|
|
|
|
+ if (!isset($dailyDevicePools[$dayTs][$deviceId])) {
|
|
|
|
|
+ // 初始化这台设备今天的标准班次池
|
|
|
|
|
+ $dailyDevicePools[$dayTs][$deviceId] = $cleanWorkRanges;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $tempRem = (int)$toAllocMin;
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 先尝试塞进标准班次
|
|
|
|
|
+ foreach ($dailyDevicePools[$dayTs][$deviceId] as &$p) {
|
|
|
|
|
+ if ($tempRem <= 0) break;
|
|
|
|
|
+
|
|
|
|
|
+ $pMax = $p['e'] - $p['s'];
|
|
|
|
|
+ if ($pMax <= 0) continue;
|
|
|
|
|
+
|
|
|
|
|
+ $take = min($tempRem, $pMax);
|
|
|
|
|
+ $start = (int)$p['s'];
|
|
|
|
|
+ $end = $start + $take;
|
|
|
|
|
+
|
|
|
|
|
+ $previewList[] = $this->buildPreviewRow(
|
|
|
|
|
+ $currentTempMainId, $dayTs, $itemId, $itemTitle,
|
|
|
|
|
+ $deviceId, $deviceMap[$deviceId] ?? '未知设备', $start, $end, $take
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ $tempRem -= $take;
|
|
|
|
|
+ $p['s'] = $end; // 指针平移
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 【关键安全阀】如果标准班次格子全满了,但 tempRem 还有剩(说明设备在冲刺加班)
|
|
|
|
|
+ // 绝对不能扔掉!从当天的最后一个结束时间开始,无上限往后顺延生成“加班行”
|
|
|
|
|
+ if ($tempRem > 0) {
|
|
|
|
|
+ // 找到当天的最后落点,如果连标准班次都没有,默认从 18:00 (1080) 开始无缝排
|
|
|
|
|
+ $lastEnd = 1080;
|
|
|
|
|
+ if (!empty($cleanWorkRanges)) {
|
|
|
|
|
+ $lastEnd = end($cleanWorkRanges)['e'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 看看之前有没有已经排上去的加班,有的话紧跟其后
|
|
|
|
|
+ if (isset($dailyDevicePools[$dayTs][$deviceId]['overtime_last_end'])) {
|
|
|
|
|
+ $lastEnd = $dailyDevicePools[$dayTs][$deviceId]['overtime_last_end'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $start = $lastEnd;
|
|
|
|
|
+ $end = $start + $tempRem;
|
|
|
|
|
+
|
|
|
|
|
+ $previewList[] = $this->buildPreviewRow(
|
|
|
|
|
+ $currentTempMainId, $dayTs, $itemId, $itemTitle,
|
|
|
|
|
+ $deviceId, $deviceMap[$deviceId] ?? '未知设备', $start, $end, $tempRem
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 记录该设备今天最新的加班落点
|
|
|
|
|
+ $dailyDevicePools[$dayTs][$deviceId]['overtime_last_end'] = $end;
|
|
|
|
|
+ $tempRem = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ['status' => true, 'data' => $previewList];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 辅助抽离:组装预览行数据
|
|
|
|
|
+ */
|
|
|
|
|
+ private function buildPreviewRow($tempMainId, $dayTs, $itemId, $itemTitle, $deviceId, $deviceTitle, $start, $end, $take)
|
|
|
|
|
+ {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'temp_main_id' => $tempMainId,
|
|
|
|
|
+ 'order_time' => date('Y-m-d', $dayTs),
|
|
|
|
|
+ 'order_timestamp' => $dayTs,
|
|
|
|
|
+ 'item_id' => $itemId,
|
|
|
|
|
+ 'item_title' => $itemTitle,
|
|
|
|
|
+ 'device_id' => $deviceId,
|
|
|
|
|
+ 'device_title' => $deviceTitle,
|
|
|
|
|
+ 'start_time' => sprintf('%02d:%02d', floor($start / 60), $start % 60),
|
|
|
|
|
+ 'end_time' => sprintf('%02d:%02d', floor($end / 60), $end % 60),
|
|
|
|
|
+ 'start_hour' => (int)floor($start / 60),
|
|
|
|
|
+ 'start_min' => (int)($start % 60),
|
|
|
|
|
+ 'end_hour' => (int)floor($end / 60),
|
|
|
|
|
+ 'end_min' => (int)($end % 60),
|
|
|
|
|
+ 'total_work_min' => $take,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public function dailyDwOrderSave($data, $user)
|
|
public function dailyDwOrderSave($data, $user)
|
|
|
{
|
|
{
|
|
|
$list = $data['list'] ?? [];
|
|
$list = $data['list'] ?? [];
|