|
|
@@ -1019,4 +1019,340 @@ class PersonWorkService extends Service
|
|
|
}
|
|
|
return $pool;
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
+ public function dailyPwOrderPreview($data, $user)
|
|
|
+ {
|
|
|
+ $topDepartId = $user['top_depart_id'];
|
|
|
+ if (empty($data['month'])) return [false, '月份不能为空'];
|
|
|
+
|
|
|
+ // 1. 前置校验 (保留你之前的校验逻辑)
|
|
|
+ $monthStart = $this->changeDateToDate($data['month']);
|
|
|
+
|
|
|
+ // 1. 检查是否存在月度工时明细
|
|
|
+ $hasMonthlyOrder = DB::table('monthly_pw_order_details as d')
|
|
|
+ ->join('monthly_pw_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)
|
|
|
+ ->exists();
|
|
|
+
|
|
|
+ if (!$hasMonthlyOrder) return [false, '未找到该月份的月度工时明细,请先生成人员月度工时单'];
|
|
|
+
|
|
|
+ // 2. 检查是否配置了工作日历
|
|
|
+ $hasCalendar = DB::table('calendar_details')
|
|
|
+ ->where('month', $monthStart)
|
|
|
+ ->where('del_time', 0)
|
|
|
+ ->exists();
|
|
|
+
|
|
|
+ if (!$hasCalendar) return [false, '该月份工作日历未配置'];
|
|
|
+
|
|
|
+ // 3. 检查是否配置了项目比例规则
|
|
|
+ $hasRules = DB::table('rule_set as r')
|
|
|
+ ->where('r.month', $monthStart)
|
|
|
+ ->where('r.top_depart_id', $topDepartId)
|
|
|
+ ->where('r.del_time', 0)
|
|
|
+ ->exists();
|
|
|
+
|
|
|
+ if (!$hasRules) return [false, '未找到该月份的规则配置单'];
|
|
|
+
|
|
|
+ // 2. 调用核心计算逻辑 (抽取出的私有方法)
|
|
|
+ $result = $this->calculateDailyAllocation($monthStart, $topDepartId, $user);
|
|
|
+
|
|
|
+ if (!$result['status']) return [false, $result['msg']];
|
|
|
+
|
|
|
+ // 3. 将结果存入临时表或直接返回
|
|
|
+ // 建议增加一个 batch_id,防止多人操作冲突
|
|
|
+ $batchId = uniqid('batch_');
|
|
|
+ $previewData = $result['data'];
|
|
|
+
|
|
|
+ return [true, [
|
|
|
+ 'batch_id' => $batchId,
|
|
|
+ 'list' => $previewData // 返回给前端展示
|
|
|
+ ]];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 核心分配逻辑:计算预览数据(确保全整数分钟)
|
|
|
+ * @param int $monthStart 月初时间戳
|
|
|
+ * @param int $topDepartId 顶级部门ID
|
|
|
+ * @param array $user 用户信息
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function calculateDailyAllocation($monthStart, $topDepartId, $user)
|
|
|
+ {
|
|
|
+ $monthEnd = strtotime('+1 month', $monthStart) - 1;
|
|
|
+ $now = time();
|
|
|
+
|
|
|
+ // --- 1. 基础数据加载 ---
|
|
|
+
|
|
|
+ // 加载月度工时明细,并关联人员姓名
|
|
|
+ $monthlyOrder = DB::table('monthly_pw_order_details as d')
|
|
|
+ ->join('monthly_pw_order as m', 'm.id', '=', 'd.main_id')
|
|
|
+ ->leftJoin('employee as e', 'e.id', '=', 'd.employee_id') // 关联人员表
|
|
|
+ ->where('m.month', $monthStart)
|
|
|
+ ->where('m.top_depart_id', $topDepartId)
|
|
|
+ ->where('m.del_time', 0)
|
|
|
+ ->where('d.del_time', 0)
|
|
|
+ ->select('d.*', 'e.title as employee_title') // 获取人员姓名
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ if ($monthlyOrder->isEmpty()) return ['status' => false, 'msg' => '未找到该月份的月度工时明细'];
|
|
|
+
|
|
|
+ // 建立人员 ID -> 姓名的映射,方便后续取用
|
|
|
+ $empNameMap = $monthlyOrder->pluck('employee_title', 'employee_id')->toArray();
|
|
|
+ $empIds = array_keys($empNameMap);
|
|
|
+
|
|
|
+ // 加载项目信息,用于获取项目名称
|
|
|
+ // 假设项目表名为 items,请根据你实际的表名修改
|
|
|
+ $itemIds = DB::table('rule_set_details as rd')
|
|
|
+ ->join('rule_set as r', 'r.id', '=', 'rd.main_id')
|
|
|
+ ->where('r.month', $monthStart)
|
|
|
+ ->where('r.top_depart_id', $topDepartId)
|
|
|
+ ->pluck('rd.item_id')->unique()->toArray();
|
|
|
+
|
|
|
+ $itemMap = DB::table('item')
|
|
|
+ ->whereIn('id', $itemIds)
|
|
|
+ ->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', 1)
|
|
|
+ ->where('r.del_time', 0)
|
|
|
+ ->where('rd.del_time', 0)
|
|
|
+ ->select('rd.*')
|
|
|
+ ->get()
|
|
|
+ ->groupBy('data_id');
|
|
|
+
|
|
|
+ // 加载员工/标准班次、日历、请假加班数据 (逻辑同前)
|
|
|
+ $empWorkRanges = DB::table('employee_work_range')->whereIn('employee_id', $empIds)->where('top_depart_id', $topDepartId)->get()->groupBy('employee_id');
|
|
|
+ $standardWorkRanges = DB::table('work_range_details')->where('top_depart_id', $topDepartId)->where('del_time', 0)->get();
|
|
|
+ $allDays = DB::table('calendar_details')->where('month', $monthStart)->where('del_time', 0)->orderBy('time', 'asc')->get();
|
|
|
+ $leaveOverData = DB::table('p_leave_over_order_details as d')->join('p_leave_over_order as m', 'd.main_id', '=', 'm.id')
|
|
|
+ ->whereBetween('m.order_time', [$monthStart, $monthEnd])->where('m.del_time', 0)
|
|
|
+ ->select('d.*', 'm.order_time', 'm.type as main_type')->get()->groupBy(['employee_id', 'order_time']);
|
|
|
+
|
|
|
+ // --- 2. 阶段一:计算每个人每天在每个项目上应分配的整数分钟数 ---
|
|
|
+ $finalAlloc = [];
|
|
|
+ foreach ($monthlyOrder as $mDetail) {
|
|
|
+ $empId = $mDetail->employee_id;
|
|
|
+ $empRules = $ruleSet->get($empId);
|
|
|
+ if (!$empRules) continue;
|
|
|
+
|
|
|
+ $empRemainingMin = (int)round((float)$mDetail->rd_total_hours * 60);
|
|
|
+ if ($empRemainingMin <= 0) continue;
|
|
|
+
|
|
|
+ foreach ($allDays as $dayInfo) {
|
|
|
+ if ($empRemainingMin <= 0) break;
|
|
|
+ $dayTs = $dayInfo->time;
|
|
|
+ $tempPool = $this->buildAvailablePool($empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData);
|
|
|
+ $dayAvailableMin = 0;
|
|
|
+ foreach ($tempPool as $p) { $dayAvailableMin += (int)($p['e'] - $p['s']); }
|
|
|
+ if ($dayAvailableMin <= 0) continue;
|
|
|
+
|
|
|
+ $canAllocToday = min($empRemainingMin, $dayAvailableMin);
|
|
|
+ $allocatedInDay = 0;
|
|
|
+ $ruleCount = count($empRules);
|
|
|
+
|
|
|
+ foreach ($empRules as $index => $rule) {
|
|
|
+ $rate = (float)$rule->rate / 100;
|
|
|
+ if ($index === $ruleCount - 1) {
|
|
|
+ $projectMin = $canAllocToday - $allocatedInDay;
|
|
|
+ } else {
|
|
|
+ $projectMin = (int)round($canAllocToday * $rate);
|
|
|
+ }
|
|
|
+ if ($projectMin > 0) {
|
|
|
+ $finalAlloc[$dayTs][$rule->item_id][$empId] = $projectMin;
|
|
|
+ $allocatedInDay += $projectMin;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $empRemainingMin -= $canAllocToday;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 3. 阶段二:打散到具体时间点并生成预览行 ---
|
|
|
+ $previewList = [];
|
|
|
+ $dailyEmpTimePools = [];
|
|
|
+ $tempMainIdCounter = 1;
|
|
|
+
|
|
|
+ foreach ($finalAlloc as $dayTs => $projects) {
|
|
|
+ foreach ($projects as $itemId => $employees) {
|
|
|
+ $currentTempMainId = $tempMainIdCounter++;
|
|
|
+
|
|
|
+ // 获取项目名称
|
|
|
+ $itemTitle = $itemMap[$itemId] ?? '未知项目';
|
|
|
+
|
|
|
+ foreach ($employees as $empId => $toAllocMin) {
|
|
|
+ if (!isset($dailyEmpTimePools[$dayTs][$empId])) {
|
|
|
+ $dailyEmpTimePools[$dayTs][$empId] = $this->buildAvailablePool($empId, $dayTs, $allDays, $empWorkRanges, $standardWorkRanges, $leaveOverData);
|
|
|
+ }
|
|
|
+
|
|
|
+ $tempRem = (int)$toAllocMin;
|
|
|
+ foreach ($dailyEmpTimePools[$dayTs][$empId] as &$p) {
|
|
|
+ if ($tempRem <= 0) break;
|
|
|
+ $pMax = (int)($p['e'] - $p['s']);
|
|
|
+ if ($pMax <= 0) continue;
|
|
|
+
|
|
|
+ $take = min($tempRem, $pMax);
|
|
|
+ $realStart = (int)$p['s'];
|
|
|
+ $realEnd = $realStart + $take;
|
|
|
+
|
|
|
+ // 写入带 Title 的结果
|
|
|
+ $previewList[] = [
|
|
|
+ 'temp_main_id' => $currentTempMainId,
|
|
|
+ 'order_time' => date('Y-m-d', $dayTs),
|
|
|
+ 'order_timestamp' => $dayTs,
|
|
|
+ 'item_id' => $itemId,
|
|
|
+ 'item_title' => $itemTitle, // 项目名称
|
|
|
+ 'employee_id' => $empId,
|
|
|
+ 'employee_title' => $empNameMap[$empId] ?? '未知人员', // 人员姓名
|
|
|
+ 'start_time' => sprintf('%02d:%02d', floor($realStart / 60), $realStart % 60),
|
|
|
+ 'end_time' => sprintf('%02d:%02d', floor($realEnd / 60), $realEnd % 60),
|
|
|
+ 'start_hour' => (int)floor($realStart / 60),
|
|
|
+ 'start_min' => (int)($realStart % 60),
|
|
|
+ 'end_hour' => (int)floor($realEnd / 60),
|
|
|
+ 'end_min' => (int)($realEnd % 60),
|
|
|
+ 'total_work_min' => $take,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $tempRem -= $take;
|
|
|
+ $p['s'] = $realEnd;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ['status' => true, 'data' => $previewList];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function dailyPwOrderSave($data, $user)
|
|
|
+ {
|
|
|
+ $list = $data['list'] ?? [];
|
|
|
+ if (empty($list)) return [false, '没有可保存的数据'];
|
|
|
+
|
|
|
+ $topDepartId = $user['top_depart_id'];
|
|
|
+ $month = $data['month']; // 格式如: "2026-03"
|
|
|
+ $now = time();
|
|
|
+
|
|
|
+ // 1. 预加载员工名称映射 (使用 title 字段)
|
|
|
+ $empIds = collect($list)->pluck('employee_id')->unique()->toArray();
|
|
|
+ $empMap = DB::table('employee')->whereIn('id', $empIds)->pluck('title', 'id')->toArray();
|
|
|
+
|
|
|
+ // --- 2. 重新分组并记录行号 ---
|
|
|
+ $groupedByOrder = [];
|
|
|
+ foreach ($list as $index => $item) {
|
|
|
+ $item['_line'] = $index + 1; // 记录原始行号(从1开始)
|
|
|
+ // 以日期字符串和项目ID作为分组 Key
|
|
|
+ $groupKey = $item['order_time'] . '_' . $item['item_id'];
|
|
|
+ $groupedByOrder[$groupKey][] = $item;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 冲突校验器容器:记录 [员工ID][日期字符串] 下已占用的时间段
|
|
|
+ $empTimeline = [];
|
|
|
+
|
|
|
+ DB::beginTransaction();
|
|
|
+ try {
|
|
|
+ // A. 清理该月份旧数据
|
|
|
+ $monthStart = $this->changeDateToDate($month);
|
|
|
+ $monthEnd = strtotime('+1 month', $monthStart) - 1;
|
|
|
+
|
|
|
+ $oldOrderIds = DB::table('daily_pw_order')
|
|
|
+ ->where('top_depart_id', $topDepartId)
|
|
|
+ ->whereBetween('order_time', [$monthStart, $monthEnd])
|
|
|
+ ->where('del_time', 0)
|
|
|
+ ->pluck('id');
|
|
|
+
|
|
|
+ if ($oldOrderIds->isNotEmpty()) {
|
|
|
+ DB::table('daily_pw_order')->whereIn('id', $oldOrderIds)->update(['del_time' => $now]);
|
|
|
+ DB::table('daily_pw_order_details')->whereIn('main_id', $oldOrderIds)->update(['del_time' => $now]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // B. 遍历重组后的分组写入
|
|
|
+ foreach ($groupedByOrder as $details) {
|
|
|
+ $first = $details[0];
|
|
|
+ // 【修正】统一将前端日期字符串转为时间戳入库
|
|
|
+ $orderTimestamp = strtotime($first['order_time']);
|
|
|
+ $itemId = $first['item_id'];
|
|
|
+
|
|
|
+ // 写入主表
|
|
|
+ $mainId = DB::table('daily_pw_order')->insertGetId([
|
|
|
+ 'code' => '',
|
|
|
+ 'item_id' => $itemId,
|
|
|
+ 'order_time' => $orderTimestamp,
|
|
|
+ 'top_depart_id' => $topDepartId,
|
|
|
+ 'is_create' => 1,
|
|
|
+ 'crt_id' => $user['id'],
|
|
|
+ 'crt_time' => $now,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $insertDetails = [];
|
|
|
+ foreach ($details as $d) {
|
|
|
+ $rowNum = $d['_line'];
|
|
|
+ $empId = $d['employee_id'];
|
|
|
+ $empName = $empMap[$empId] ?? "人员(ID:{$empId})";
|
|
|
+
|
|
|
+ // --- 新增:月份一致性校验 ---
|
|
|
+ // 校验这行数据的日期是否属于当前保存的月份
|
|
|
+ if (date("Y-m", strtotime($d['order_time'])) !== $month) {
|
|
|
+ return [false, "第 {$rowNum} 行:人员[{$empName}]的日期[{$d['order_time']}]不属于保存月份[{$month}]"];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【修正】强制由后端计算分钟数
|
|
|
+ $s = (int)$d['start_hour'] * 60 + (int)$d['start_min'];
|
|
|
+ $e = (int)$d['end_hour'] * 60 + (int)$d['end_min'];
|
|
|
+ $calcTotalMin = $e - $s;
|
|
|
+
|
|
|
+ // 校验1:逻辑合法性
|
|
|
+ if ($calcTotalMin <= 0) {
|
|
|
+ return [false, "第 {$rowNum} 行:人员[{$empName}]在[{$d['order_time']}]的时间段无效:结束时间必须晚于开始时间"];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验2:跨单据时间重叠校验
|
|
|
+ $dateStr = $d['order_time'];
|
|
|
+ if (isset($empTimeline[$empId][$dateStr])) {
|
|
|
+ foreach ($empTimeline[$empId][$dateStr] as $exist) {
|
|
|
+ if ($s < $exist['e'] && $e > $exist['s']) {
|
|
|
+ return [false, "第 {$rowNum} 行:人员[{$empName}]在[{$dateStr}]存在时间冲突({$d['start_time']}-{$d['end_time']}),请检查!"];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $empTimeline[$empId][$dateStr][] = ['s' => $s, 'e' => $e];
|
|
|
+
|
|
|
+ $insertDetails[] = [
|
|
|
+ 'main_id' => $mainId,
|
|
|
+ 'employee_id' => $empId,
|
|
|
+ 'top_depart_id' => $topDepartId,
|
|
|
+ 'start_time_hour' => $d['start_hour'],
|
|
|
+ 'start_time_min' => $d['start_min'],
|
|
|
+ 'end_time_hour' => $d['end_hour'],
|
|
|
+ 'end_time_min' => $d['end_min'],
|
|
|
+ 'total_work_min' => $calcTotalMin,
|
|
|
+ 'crt_time' => $now,
|
|
|
+ 'order_time' => $orderTimestamp,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量写入明细
|
|
|
+ DB::table('daily_pw_order_details')->insert($insertDetails);
|
|
|
+
|
|
|
+ // C. 回填单号
|
|
|
+ $code = $this->generateBillNo([
|
|
|
+ 'top_depart_id' => $topDepartId,
|
|
|
+ 'type' => DailyPwOrder::Order_type,
|
|
|
+ 'period' => date("Ym", $orderTimestamp)
|
|
|
+ ]);
|
|
|
+ DB::table('daily_pw_order')->where('id', $mainId)->update(['code' => $code]);
|
|
|
+ }
|
|
|
+
|
|
|
+ DB::commit();
|
|
|
+ return [true, '保存成功'];
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ DB::rollBack();
|
|
|
+ return [false, "保存失败:" . $e->getMessage()];
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|