Browse Source

小高薪

cqp 3 months ago
parent
commit
7ee77d587e

+ 76 - 0
app/Http/Controllers/Api/PLeaveOverController.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Service\PLeaveOverService;
+use Illuminate\Http\Request;
+
+class PLeaveOverController extends BaseController
+{
+    public function pLeaveOverEdit(Request $request)
+    {
+        $service = new PLeaveOverService();
+        $user = $request->userData;
+        list($status,$data) = $service->pLeaveOverEdit($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function pLeaveOverAdd(Request $request)
+    {
+        $service = new PLeaveOverService();
+        $user = $request->userData;
+        list($status,$data) = $service->pLeaveOverAdd($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function pLeaveOverDel(Request $request)
+    {
+        $service = new PLeaveOverService();
+        $user = $request->userData;
+        list($status,$data) = $service->pLeaveOverDel($request->all());
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function pLeaveOverList(Request $request)
+    {
+        $service = new PLeaveOverService();
+        $user = $request->userData;
+        list($status,$data) = $service->pLeaveOverList($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function pLeaveOverDetail(Request $request)
+    {
+        $service = new PLeaveOverService();
+        $user = $request->userData;
+        list($status,$data) = $service->pLeaveOverDetail($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+}

+ 13 - 0
app/Http/Controllers/Api/PersonWorkController.php

@@ -140,4 +140,17 @@ class PersonWorkController extends BaseController
             return $this->json_return(201,$data);
         }
     }
+
+    public function dailyPwOrderCreate(Request $request)
+    {
+        $service = new PersonWorkService();
+        $user = $request->userData;
+        list($status,$data) = $service->dailyPwOrderCreate($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
 }

+ 22 - 0
app/Model/PLeaveOverOrder.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Model;
+
+class PLeaveOverOrder extends DataScopeBaseModel
+{
+    protected $table = "p_leave_over_order"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    const employee_column = "crt_id";
+    const Order_type = "p_leave_over_order";
+
+    public static $field = ['*'];
+
+    const TYPE_ONE = 1; // 请假单
+    const TYPE_TWO = 2; // 加班单
+    const Type = [
+        self::TYPE_ONE => '请假单',
+        self::TYPE_TWO => '加班单',
+    ];
+}

+ 18 - 0
app/Model/PLeaveOverOrderDetails.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Model;
+
+class PLeaveOverOrderDetails extends DataScopeBaseModel
+{
+    protected $table = "p_leave_over_order_details"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+
+    const TYPE_ONE = 1; // 请假单
+    const TYPE_TWO = 2; // 加班单
+    const Type = [
+        self::TYPE_ONE => '请假单',
+        self::TYPE_TWO => '加班单',
+    ];
+}

+ 11 - 0
app/Model/WorkRangeDetails.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Model;
+
+class WorkRangeDetails extends DataScopeBaseModel
+{
+    protected $table = "work_range_details"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+}

+ 39 - 0
app/Service/ExportFileService.php

@@ -4,6 +4,7 @@ namespace App\Service;
 
 use App\Exports\ExportOrder;
 use App\Exports\MultiSheetExport;
+use App\Model\PLeaveOverOrder;
 use Maatwebsite\Excel\Facades\Excel;
 
 class ExportFileService extends Service
@@ -317,6 +318,44 @@ class ExportFileService extends Service
         return [true, $this->saveExportData($return,$header)];
     }
 
+    public function leaveOrder($ergs,$user){
+        // 导出数据
+        $return = [];
+        $header_default = $user['e_header_default'];
+        $column = array_column($header_default,'export');
+        $header = array_column($header_default,'value');
+
+        $service = new PLeaveOverService();
+        $ergs['type'] = PLeaveOverOrder::TYPE_ONE;
+        $model = $service->pLeaveOverCommon($ergs, $user);
+
+        $model->chunk(500,function ($data) use(&$return, $service, $column){
+            // 直接处理这一批主表数据,将其与详情合并平铺
+            $service->fillDataForExportDaily($data->toArray(), $column, $return);
+        });
+
+        return [true, $this->saveExportData($return,$header)];
+    }
+
+    public function overtimeOrder($ergs,$user){
+        // 导出数据
+        $return = [];
+        $header_default = $user['e_header_default'];
+        $column = array_column($header_default,'export');
+        $header = array_column($header_default,'value');
+
+        $service = new PLeaveOverService();
+        $ergs['type'] = PLeaveOverOrder::TYPE_TWO;
+        $model = $service->pLeaveOverCommon($ergs, $user);
+
+        $model->chunk(500,function ($data) use(&$return, $service, $column){
+            // 直接处理这一批主表数据,将其与详情合并平铺
+            $service->fillDataForExportDaily($data->toArray(), $column, $return);
+        });
+
+        return [true, $this->saveExportData($return,$header)];
+    }
+
     public function saveExportData($data, $headers, $type = 'default',$file_name = ''){
         if(empty($file_name)) $file_name = self::$filename . "_". date("Y-m-d") . "_". rand(1000,9999);
         $filename =  $file_name . '.' . 'xlsx';

+ 512 - 319
app/Service/ImportService.php

@@ -16,6 +16,7 @@ use App\Model\ItemDetails;
 use App\Model\MonthlyDdOrder;
 use App\Model\MonthlyPsOrder;
 use App\Model\MonthlyPwOrder;
+use App\Model\PLeaveOverOrder;
 use App\Model\RuleSet;
 use App\Model\RuleSetDetails;
 use Illuminate\Support\Facades\DB;
@@ -39,6 +40,8 @@ class ImportService extends Service
         'ruleSet', // 规则配置单
         'dailyPwOrder', // 人员日工时单
         'dailyDwOrder', // 设备日工时单
+        'leaveOrder', // 请假单
+        'overtimeOrder', // 加班单
     ];
 
     public function getTableTitleXls($data,$user){
@@ -380,77 +383,6 @@ class ImportService extends Service
         return [$error_str, $update_mapping, $detail_storage];
     }
 
-    private function employeeCheck111(&$array, $user, $table_config)
-    {
-        $keys = array_column($table_config, 'key');
-        $codeIdx = array_search('number', $keys);
-        $sexIdx = array_search('sex', $keys);
-        $eductionIdx = array_search('education', $keys);
-        $stateIdx = array_search('state', $keys);
-        $depIdx = array_search('depart_code', $keys);
-
-        $code_map = $this->getEmployeeList($array, $user, $codeIdx);
-        $dep_map = $this->getEDataList($array, $user, $depIdx);
-
-        $errors = [];
-        $update_mapping = [];
-        $detail_storage = [];
-        $sex_map = array_flip(Employee::SEX_TYPE);
-        $e_map = array_flip(Employee::Education);
-        $state_map = array_flip(Employee::State_Type);
-
-        foreach ($array as $rowIndex => $rowValue) {
-            $displayLine = $rowIndex + 1;
-
-            $valCode = $rowValue[$codeIdx] ?? '';
-
-            // 1. 判定更新还是新增
-            if (isset($code_map[$valCode])) {
-                $update_mapping[$rowIndex] = $code_map[$valCode];
-            }
-
-            // 2. 校验
-            $sex_text = $rowValue[$sexIdx] ?? '';
-            if(! empty($sex_text)){
-                if (!isset($sex_map[$sex_text])) {
-                    $errors[] = "第{$displayLine}行:性别[{$sex_text}]无效";
-                } else {
-                    $array[$rowIndex][$sexIdx] = $sex_map[$sex_text];
-                }
-            }
-            $e_text = $rowValue[$eductionIdx] ?? '';
-            if(! empty($e_text)){
-                if (!isset($e_map[$e_text])) {
-                    $errors[] = "第{$displayLine}行:性别[{$e_text}]无效";
-                } else {
-                    $array[$rowIndex][$eductionIdx] = $e_map[$e_text];
-                }
-            }
-            $state_text = $rowValue[$stateIdx] ?? '';
-            if (!isset($state_map[$state_text])) {
-                $errors[] = "第{$displayLine}行:状态[{$state_text}]无效";
-            } else {
-                $array[$rowIndex][$stateIdx] = $state_map[$state_text];
-            }
-
-            // 4. 解析部门 (分行模式:一行一个部门)
-            if ($depIdx !== false && !empty($rowValue[$depIdx])) {
-                $mNum = trim($rowValue[$depIdx]);
-                if (!isset($dep_map[$mNum])) {
-                    $errors[] = "第{$displayLine}行:部门编码[{$mNum}]不存在";
-                } else {
-                    // 依然按行索引存,方便 Import 方法对应
-                    $detail_storage[$rowIndex][] = [
-                        'depart_id' => $dep_map[$mNum],
-                    ];
-                }
-            }
-        }
-
-        $error_str = !empty($errors) ? implode('|', $errors) : "";
-        return [$error_str, $update_mapping, $detail_storage];
-    }
-
     private function getEDataList($array, $user, $index1)
     {
         $depNums = [];
@@ -840,234 +772,6 @@ class ImportService extends Service
     }
 
     // 项目 -----------------------------------
-    public function itemImport1($array, $user, $other_param)
-    {
-        $upload = $array[0];
-        list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
-        if (!$status) return [false, $msg];
-        $table_config = $msg;
-
-        unset($array[0]);
-        if (empty($array)) return [false, '导入数据不能为空'];
-
-        // 1. 公共校验 (必填、唯一性等)
-        list($array, $error) = $this->checkCommon($array, $table_config);
-        if (!empty($error)) return [0, $error];
-
-        // 2. 业务详细校验 (获取更新映射及明细数据)
-        list($error, $update_map, $detail_data_map) = $this->itemCheck($array, $user, $table_config);
-        if (!empty($error)) return [0, $error];
-
-        $time = time();
-        $insert_data = [];
-        $update_data = [];
-        $all_detail_insert = [];
-        $update_main_ids = [];
-
-        // 3. 数据分拣
-        foreach ($array as $key => $value) {
-            $main_tmp = [];
-            foreach ($value as $k => $val) {
-                if (!empty($table_config[$k]['is_main'])) {
-                    $main_tmp[$table_config[$k]['key']] = $val;
-                }
-            }
-
-            if (isset($update_map[$key])) {
-                // 更新逻辑
-                $itemId = $update_map[$key];
-                $update_data[] = array_merge($main_tmp, ['id' => $itemId]);
-                $update_main_ids[] = $itemId;
-
-                // 收集明细 (后续统一插入)
-                if (isset($detail_data_map[$key])) {
-                    foreach ($detail_data_map[$key] as $d) {
-                        $all_detail_insert[] = array_merge($d, ['item_id' => $itemId, 'crt_time' => $time, 'top_depart_id' => $user['top_depart_id']]);
-                    }
-                }
-            } else {
-                // 新增逻辑
-                $main_tmp['top_depart_id'] = $user['top_depart_id'];
-                $main_tmp['crt_id'] = $user['id'];
-                $main_tmp['crt_time'] = $time;
-                // 以 code 为键,方便后续回填 ID
-                $insert_data[$main_tmp['code']] = $main_tmp;
-
-                if (isset($detail_data_map[$key])) {
-                    foreach ($detail_data_map[$key] as $d) {
-                        $all_detail_insert[] = array_merge($d, ['_code' => $main_tmp['code'], 'crt_time' => $time, 'top_depart_id' => $main_tmp['top_depart_id']]);
-                    }
-                }
-            }
-        }
-
-        DB::beginTransaction();
-        try {
-            // 4. 执行新增主表
-            if (!empty($insert_data)) {
-                foreach (array_chunk($insert_data, 500) as $chunk) {
-                    Item::insert($chunk);
-                }
-                // 获取新插入数据的 ID 映射
-                $new_item_maps = Item::whereIn('code', array_keys($insert_data))
-                    ->where('del_time', 0)
-                    ->where('top_depart_id', $user['top_depart_id'])
-                    ->pluck('id', 'code')->toArray();
-            }
-
-            // 5. 执行更新主表 (分批更新)
-            if (!empty($update_data)) {
-                foreach (array_chunk($update_data, 100) as $chunk) {
-                    foreach ($chunk as $uItem) {
-                        $id = $uItem['id'];
-                        unset($uItem['id']);
-                        Item::where('id', $id)->update($uItem);
-                    }
-                }
-            }
-
-            // 6. 处理明细表 (先删后插策略)
-            // 删除旧明细 (逻辑删除)
-            if (!empty($update_main_ids)) {
-                ItemDetails::whereIn('item_id', $update_main_ids)
-                    ->where('del_time', 0)
-                    ->update(['del_time' => $time]);
-            }
-
-            // 回填新增主表的 ID 到明细数组
-            foreach ($all_detail_insert as &$di) {
-                if (isset($di['_code'])) {
-                    $di['item_id'] = $new_item_maps[$di['_code']] ?? 0;
-                    unset($di['_code']);
-                }
-            }
-            unset($di);
-
-            // 批量插入所有明细
-            if (!empty($all_detail_insert)) {
-                foreach (array_chunk($all_detail_insert, 500) as $chunk) {
-                    ItemDetails::insert($chunk);
-                }
-            }
-
-            DB::commit();
-        } catch (\Exception $e) {
-            DB::rollBack();
-            return [false, "失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
-        }
-
-        return [true, ''];
-    }
-
-    private function itemCheck1(&$array, $user, $table_config)
-    {
-        $keys = array_column($table_config, 'key');
-        $codeIdx = array_search('code', $keys);
-        $stateIdx = array_search('state', $keys);
-        $manIdx = array_search('man_list', $keys);
-        $deviceIdx = array_search('device_list', $keys);
-        // 日期索引
-        $dateIdx = array_search('start_time', $keys);
-        $date2Idx = array_search('end_time', $keys);
-
-        $code_map = $this->getItemList($array, $user, $codeIdx);
-        list($man_map, $device_map) = $this->getDataList($array, $user, $manIdx, $deviceIdx);
-
-        $errors = [];
-        $update_mapping = [];
-        $detail_storage = [];
-        $state_type_map = array_flip(Item::State_Type);
-
-        foreach ($array as $rowIndex => $rowValue) {
-            $displayLine = $rowIndex + 1;
-
-            $valCode = $rowValue[$codeIdx] ?? '';
-
-            // 1. 判定更新还是新增
-            if (isset($code_map[$valCode])) {
-                $update_mapping[$rowIndex] = $code_map[$valCode];
-            }
-
-            // 2. 状态校验
-            $state_text = $rowValue[$stateIdx] ?? '';
-            if (!isset($state_type_map[$state_text])) {
-                $errors[] = "第{$displayLine}行:状态[{$state_text}]无效";
-            } else {
-                $array[$rowIndex][$stateIdx] = $state_type_map[$state_text];
-            }
-
-            // 3. 日期转换
-            foreach ([$dateIdx, $date2Idx] as $dIdx) {
-                if ($dIdx !== false && !empty($rowValue[$dIdx])) {
-                    list($s, $m) = $this->convertExcelCellToDate($rowValue[$dIdx]);
-                    if (!$s) $errors[] = "第{$displayLine}行:日期格式非法";
-                    else $array[$rowIndex][$dIdx] = $m;
-                }
-            }
-
-            // 4. 解析人员 (明细类型1)
-            if ($manIdx !== false && !empty($rowValue[$manIdx])) {
-                foreach (explode(',', $rowValue[$manIdx]) as $mNum) {
-                    $mNum = trim($mNum);
-                    if (!isset($man_map[$mNum])) {
-                        $errors[] = "第{$displayLine}行:人员工号[{$mNum}]不存在";
-                    } else {
-                        $detail_storage[$rowIndex][] = [
-                            'type' => ItemDetails::type_one,
-                            'data_id' => $man_map[$mNum],
-                        ];
-                    }
-                }
-            }
-
-            // 5. 解析设备 (明细类型2)
-            if ($deviceIdx !== false && !empty($rowValue[$deviceIdx])) {
-                foreach (explode(',', $rowValue[$deviceIdx]) as $dCode) {
-                    $dCode = trim($dCode);
-                    if (!isset($device_map[$dCode])) {
-                        $errors[] = "第{$displayLine}行:设备的资产编码[{$dCode}]不存在";
-                    } else {
-                        $detail_storage[$rowIndex][] = [
-                            'type' => ItemDetails::type_two,
-                            'data_id' => $device_map[$dCode],
-                        ];
-                    }
-                }
-            }
-        }
-
-        $error_str = !empty($errors) ? implode('|', $errors) : "";
-        return [$error_str, $update_mapping, $detail_storage];
-    }
-
-    private function getDataList($array, $user, $index1, $index2)
-    {
-        $manNums = [];
-        $devCodes = [];
-
-        // 去重收集
-        foreach ($array as $row) {
-            if (!empty($row[$index1])) {
-                foreach (explode(',', $row[$index1]) as $v) $manNums[trim($v)] = true;
-            }
-            if (!empty($row[$index2])) {
-                foreach (explode(',', $row[$index2]) as $v) $devCodes[trim($v)] = true;
-            }
-        }
-
-        $manMap = Employee::where('del_time', 0)
-            ->where('top_depart_id', $user['top_depart_id'])
-            ->whereIn('number', array_keys($manNums))
-            ->pluck('id', 'number')->toArray();
-
-        $devMap = Device::where('del_time', 0)
-            ->where('top_depart_id', $user['top_depart_id'])
-            ->whereIn('code', array_keys($devCodes))
-            ->pluck('id', 'code')->toArray();
-
-        return [$manMap, $devMap];
-    }
-
     private function getItemList($array, $user, $index){
         //查找设备
         $codes = array_unique(array_filter(array_column($array,$index)));
@@ -1686,8 +1390,6 @@ class ImportService extends Service
         $codeIdx = array_search('code', $keys);
         $monthIdx = array_search('month', $keys);
         $empIdx = array_search('employee_id', $keys);
-        $startIdx = array_search('start_time', $keys);
-        $endIdx = array_search('end_time', $keys);
 
         $numIdx = array_search('total_days', $keys);
         $num2Idx = array_search('rd_total_days', $keys);
@@ -1794,24 +1496,6 @@ class ImportService extends Service
                 $excelAggregator[$aggKey]['emps'][$valEmp] = true;
             }
 
-            // F. 日期逻辑检查
-            list($sStatus, $startTime) = $this->convertExcelCellToDate($row[$startIdx] ?? '');
-            list($eStatus, $endTime)   = $this->convertExcelCellToDate($row[$endIdx] ?? '');
-            if (!$sStatus || !$eStatus) {
-                $errors[] = "第{$displayLine}行:日期格式无效";
-            } else {
-                if ($startTime > $endTime) {
-                    $errors[] = "第{$displayLine}行:开始日期大于结束日期";
-                } else {
-                    $monthEndTs = strtotime("+1 month", $valMonthTs) - 1;
-                    if ($startTime < $valMonthTs || $endTime > $monthEndTs) {
-                        $errors[] = "第{$displayLine}行:日期超出月度范围";
-                    }
-                }
-                $array[$rowIndex][$startIdx] = $startTime;
-                $array[$rowIndex][$endIdx]   = $endTime;
-            }
-
             // G. 数字有效性校验
             $numFields = [
                 $numIdx => '出勤天数', $num2Idx => '研发天数',
@@ -3329,6 +3013,515 @@ class ImportService extends Service
         return [$error_string, $update_map, $dbDevices, $dbItems];
     }
 
+    // 人员请假单 --------------------------------
+    public function leaveOrderImport($array, $user, $other_param)
+    {
+        $upload = $array[0];
+        list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
+        if (!$status) return [false, $msg];
+        $table_config = $msg;
+
+        unset($array[0]);
+        if (empty($array)) return [false, '导入数据不能为空'];
+
+        // 1. 基础格式检查
+        list($array, $error) = $this->checkCommon($array, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        // 2. 详细业务校验 (固定为请假单校验)
+        list($error, $update_map, $dbEmps) = $this->leaveOrderCheck($array, $user, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $timeIdx = array_search('order_time', $keys);
+        $empIdx  = array_search('employee_id', $keys);
+        $startIdx = array_search('start_time', $keys);
+        $endIdx   = array_search('end_time', $keys);
+
+        // 3. 数据聚合分组 (按日期聚合)
+        $groups = [];
+        foreach ($array as $rowIndex => $row) {
+            $cTimeTs = $row[$timeIdx];
+            $mainId = $update_map[$rowIndex] ?? 0;
+
+            // 聚合Key:如果是更新则按主表ID;如果是新增则按日期
+            $aggKey = $mainId > 0 ? "ID_" . $mainId : "DATE_" . $cTimeTs;
+
+            if (!isset($groups[$aggKey])) {
+                $groups[$aggKey] = [
+                    'main_id'    => $mainId,
+                    'order_time' => $cTimeTs,
+                    'details'    => []
+                ];
+            }
+
+            list($sH, $sM) = $this->parseTimeHourMin($row[$startIdx]);
+            list($eH, $eM) = $this->parseTimeHourMin($row[$endIdx]);
+
+            $groups[$aggKey]['details'][] = [
+                'employee_id'     => $dbEmps[trim($row[$empIdx])] ?? 0,
+                'start_time_hour' => $sH,
+                'start_time_min'  => $sM,
+                'end_time_hour'   => $eH,
+                'end_time_min'    => $eM,
+                'total_min'       => ($eH * 60 + $eM) - ($sH * 60 + $sM),
+                'type'            => PLeaveOverOrder::TYPE_ONE, // 固定请假类型
+            ];
+        }
+
+        // 4. 执行写入
+        DB::beginTransaction();
+        try {
+            $time = time();
+            foreach ($groups as $group) {
+                if ($group['main_id'] > 0) {
+                    $mainId = $group['main_id'];
+                    // 删除旧明细准备覆盖
+                    DB::table('p_leave_over_order_details')->where('main_id', $mainId)->update(['del_time' => $time]);
+                } else {
+                    // 生成请假单号
+                    $newCode = $this->generateBillNo([
+                        'top_depart_id' => $user['top_depart_id'],
+                        'type'          => PLeaveOverOrder::Order_type,
+                        'period'        => date("Ym", $group['order_time'])
+                    ]);
+                    $mainId = DB::table('p_leave_over_order')->insertGetId([
+                        'code'          => $newCode,
+                        'order_time'    => $group['order_time'],
+                        'type'          => PLeaveOverOrder::TYPE_ONE,
+                        'top_depart_id' => $user['top_depart_id'],
+                        'crt_id'        => $user['id'],
+                        'crt_time'      => $time,
+                    ]);
+                }
+
+                foreach ($group['details'] as &$d) {
+                    $d['main_id'] = $mainId;
+                    $d['top_depart_id'] = $user['top_depart_id'];
+                    $d['crt_time'] = $time;
+                }
+                DB::table('p_leave_over_order_details')->insert($group['details']);
+            }
+            DB::commit();
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "失败:" . $e->getMessage()];
+        }
+        return [true, ''];
+    }
+
+    private function leaveOrderCheck(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $timeIdx = array_search('order_time', $keys);
+        $empIdx  = array_search('employee_id', $keys);
+        $startIdx = array_search('start_time', $keys);
+        $endIdx   = array_search('end_time', $keys);
+
+        $topDepartId = $user['top_depart_id'];
+        $errors = [];
+        $update_map = [];
+
+        // --- 1. 基础档案预加载 (代码省略,保持原样) ---
+        $empCodes = array_filter(array_unique(array_column($array, $empIdx)));
+        $empModels = Employee::where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('number', $empCodes)->get(['id', 'number', 'title']);
+        $dbEmps = $empModels->pluck('id', 'number')->toArray();
+        $empDisplayMap = $empModels->mapWithKeys(fn($item) => [$item->id => "[{$item->number}]{$item->title}"])->toArray();
+
+        // --- 2. 日历 & 主表预查 (代码省略,保持原样) ---
+        $allDateTs = [];
+        foreach ($array as $row) {
+            list($status, $ts) = $this->convertExcelCellToDate($row[$timeIdx] ?? '');
+            if ($status) $allDateTs[] = strtotime(date('Y-m-d', $ts));
+        }
+        $allDateTs = array_unique($allDateTs);
+        $calendarMap = DB::table('calendar_details')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('time', $allDateTs)->pluck('is_work', 'time')->toArray();
+
+        $existMainOrders = DB::table('p_leave_over_order')
+            ->where('top_depart_id', $topDepartId)
+            ->whereIn('order_time', $allDateTs)
+            ->where('type', PLeaveOverOrder::TYPE_ONE)
+            ->where('del_time', 0)->get()->keyBy('order_time');
+
+        // --- 3. 工时设置 (代码省略,保持原样) ---
+        $specialWorkMap = DB::table('employee_work_range')->whereIn('employee_id', array_values($dbEmps))
+            ->where('top_depart_id', $topDepartId)->get()->groupBy('employee_id');
+        $commonWorkRanges = DB::table('work_range_details')->where('del_time', 0)->where('top_depart_id', $topDepartId)->get();
+        $commonPeriods = [];
+        foreach ($commonWorkRanges as $wr) {
+            $commonPeriods[] = ['s' => $wr->start_time_hour * 60 + $wr->start_time_min, 'e' => $wr->end_time_hour * 60 + $wr->end_time_min];
+        }
+
+        // --- 4. 库内明细校验数据 (代码省略,保持原样) ---
+        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
+        $dbExistingWork = DB::table('p_leave_over_order_details as d')
+            ->join('p_leave_over_order as m', 'd.main_id', '=', 'm.id')
+            ->where('m.top_depart_id', $topDepartId)
+            ->whereIn('m.order_time', $allDateTs)
+            ->where('m.type', PLeaveOverOrder::TYPE_ONE)
+            ->where('m.del_time', 0)->where('d.del_time', 0)
+            ->whereNotIn('m.code', $allCodes)
+            ->select('m.order_time', 'd.employee_id', 'd.start_time_hour', 'd.start_time_min', 'd.end_time_hour', 'd.end_time_min')
+            ->get()->groupBy(fn($item) => $item->order_time . '_' . $item->employee_id);
+
+        $dbOrders = DB::table('p_leave_over_order')->where('top_depart_id', $topDepartId)
+            ->whereIn('code', $allCodes)->where('del_time', 0)->get()->keyBy('code');
+
+        $internalOverlapMap = [];
+
+        // --- 5. 循环校验 (全量提示版) ---
+        foreach ($array as $rowIndex => $row) {
+            $line = $rowIndex + 1;
+            $rowErrors = []; // 临时收集本行的所有错误
+
+            list($status, $ts) = $this->convertExcelCellToDate($row[$timeIdx] ?? '');
+            $ts = $status ? strtotime(date('Y-m-d', $ts)) : 0;
+            $array[$rowIndex][$timeIdx] = $ts;
+
+            if (!$ts) {
+                $errors[] = "第{$line}行:日期格式错误";
+                continue;
+            }
+
+            // 1. 单据与编辑逻辑检查
+            $valCode = trim($row[$codeIdx] ?? '');
+            if ($valCode === '') {
+                if (isset($existMainOrders[$ts])) {
+                    $rowErrors[] = date('Y-m-d', $ts) . "已存在请假单[{$existMainOrders[$ts]->code}],请填写单号进行编辑";
+                }
+            } else {
+                if (isset($dbOrders[$valCode])) {
+                    if ($dbOrders[$valCode]->order_time != $ts) {
+                        $rowErrors[] = "单据[{$valCode}]原日期不符,不允许跨日期编辑";
+                    }
+                    $update_map[$rowIndex] = $dbOrders[$valCode]->id;
+                } else {
+                    $rowErrors[] = "单据号[{$valCode}]不存在";
+                }
+            }
+
+            // 2. 日历检查
+            if (!isset($calendarMap[$ts]) || (int)$calendarMap[$ts] !== 1) {
+                $rowErrors[] = "该日期非工作日,无需请假";
+            }
+
+            // 3. 人员检查
+            $empId = $dbEmps[trim($row[$empIdx])] ?? 0;
+            $empName = $empDisplayMap[$empId] ?? "工号:".$row[$empIdx];
+            if (!$empId) {
+                $rowErrors[] = "人员不存在";
+                // 如果人都不存在,后面的时间校验没意义,报错合并
+                $errors[] = "第{$line}行:" . implode(';', $rowErrors);
+                continue;
+            }
+
+            // 4. 时间段合法性检查
+            list($sH, $sM) = $this->parseTimeHourMin($row[$startIdx] ?? '');
+            list($eH, $eM) = $this->parseTimeHourMin($row[$endIdx] ?? '');
+            $curS = $sH * 60 + $sM; $curE = $eH * 60 + $eM;
+
+            if ($curS >= $curE) {
+                $rowErrors[] = "结束时间需晚于开始时间";
+            } else {
+                // 只有在时间段本身合法的情况下,才检查范围和重叠
+
+                // A. 工作范围检查
+                $empPeriods = isset($specialWorkMap[$empId]) ? [] : $commonPeriods;
+                if (isset($specialWorkMap[$empId])) {
+                    foreach ($specialWorkMap[$empId] as $sw) {
+                        $empPeriods[] = ['s' => $sw->start_time_hour * 60 + $sw->start_time_min, 'e' => $sw->end_time_hour * 60 + $sw->end_time_min];
+                    }
+                }
+                $inRange = false;
+                foreach ($empPeriods as $p) { if ($curS >= $p['s'] && $curE <= $p['e']) { $inRange = true; break; } }
+                if (!$inRange) {
+                    $rowErrors[] = "请假需在规定的工作时间内";
+                }
+
+                // B. 重叠检查 (不再 continue,两个都查)
+                $checkKey = $ts . "_" . $empId;
+                // 表内
+                if (isset($internalOverlapMap[$checkKey])) {
+                    foreach ($internalOverlapMap[$checkKey] as $p) {
+                        if ($curS < $p['e'] && $p['s'] < $curE) {
+                            $rowErrors[] = "在表内存在时间重叠";
+                            break; // 同一行的“表内重叠”只提示一次
+                        }
+                    }
+                }
+                // 库内
+                if (isset($dbExistingWork[$checkKey])) {
+                    foreach ($dbExistingWork[$checkKey] as $p) {
+                        $exS = $p->start_time_hour * 60 + $p->start_time_min;
+                        $exE = $p->end_time_hour * 60 + $p->end_time_min;
+                        if ($curS < $exE && $exS < $curE) {
+                            $rowErrors[] = "与已有的请假单时间重叠";
+                            break; // 同一行的“库内重叠”只提示一次
+                        }
+                    }
+                }
+            }
+
+            // 收集汇总错误
+            if (!empty($rowErrors)) {
+                $errors[] = "第{$line}行:{$empName}" . implode(',', $rowErrors);
+            } else {
+                // 只有完全没错误的数据,才放入 internalOverlapMap 供后续行比对
+                // 这样可以防止第2行报错了,第3行又提示跟第2行重叠,造成提示混乱
+                $checkKey = $ts . "_" . $empId;
+                $internalOverlapMap[$checkKey][] = ['s' => $curS, 'e' => $curE];
+            }
+        }
+
+        return [!empty($errors) ? implode('|', $errors) : "", $update_map, $dbEmps];
+    }
+
+    // 人员加班单
+    public function overtimeOrderImport($array, $user, $other_param)
+    {
+        $upload = $array[0];
+        list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
+        if (!$status) return [false, $msg];
+        $table_config = $msg;
+
+        unset($array[0]);
+        if (empty($array)) return [false, '导入数据不能为空'];
+
+        // 1. 基础格式检查
+        list($array, $error) = $this->checkCommon($array, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        // 2. 业务逻辑校验 (专用于加班)
+        list($error, $update_map, $dbEmps) = $this->overtimeOrderCheck($array, $user, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $timeIdx = array_search('order_time', $keys);
+        $empIdx  = array_search('employee_id', $keys);
+        $startIdx = array_search('start_time', $keys);
+        $endIdx   = array_search('end_time', $keys);
+
+        // 3. 数据聚合分组 (按日期聚合)
+        $groups = [];
+        foreach ($array as $rowIndex => $row) {
+            $cTimeTs = $row[$timeIdx];
+            $mainId = $update_map[$rowIndex] ?? 0;
+            $aggKey = $mainId > 0 ? "ID_" . $mainId : "DATE_" . $cTimeTs;
+
+            if (!isset($groups[$aggKey])) {
+                $groups[$aggKey] = [
+                    'main_id'    => $mainId,
+                    'order_time' => $cTimeTs,
+                    'details'    => []
+                ];
+            }
+
+            list($sH, $sM) = $this->parseTimeHourMin($row[$startIdx]);
+            list($eH, $eM) = $this->parseTimeHourMin($row[$endIdx]);
+
+            $groups[$aggKey]['details'][] = [
+                'employee_id'     => $dbEmps[trim($row[$empIdx])] ?? 0,
+                'start_time_hour' => $sH,
+                'start_time_min'  => $sM,
+                'end_time_hour'   => $eH,
+                'end_time_min'    => $eM,
+                'total_min'       => ($eH * 60 + $eM) - ($sH * 60 + $sM),
+                'type'            => PLeaveOverOrder::TYPE_TWO, // 加班类型
+            ];
+        }
+
+        // 4. 执行写入
+        DB::beginTransaction();
+        try {
+            $time = time();
+            foreach ($groups as $group) {
+                if ($group['main_id'] > 0) {
+                    $mainId = $group['main_id'];
+                    DB::table('p_leave_over_order_details')->where('main_id', $mainId)->update(['del_time' => $time]);
+                } else {
+                    $newCode = $this->generateBillNo([
+                        'top_depart_id' => $user['top_depart_id'],
+                        'type'          => PLeaveOverOrder::Order_type,
+                        'period'        => date("Ym", $group['order_time'])
+                    ]);
+                    $mainId = DB::table('p_leave_over_order')->insertGetId([
+                        'code'          => $newCode,
+                        'order_time'    => $group['order_time'],
+                        'type'          => PLeaveOverOrder::TYPE_TWO,
+                        'top_depart_id' => $user['top_depart_id'],
+                        'crt_id'        => $user['id'],
+                        'crt_time'      => $time,
+                    ]);
+                }
+
+                foreach ($group['details'] as &$d) {
+                    $d['main_id'] = $mainId;
+                    $d['top_depart_id'] = $user['top_depart_id'];
+                    $d['crt_time'] = $time;
+                }
+                DB::table('p_leave_over_order_details')->insert($group['details']);
+            }
+            DB::commit();
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "失败:" . $e->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    private function overtimeOrderCheck(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $timeIdx = array_search('order_time', $keys);
+        $empIdx  = array_search('employee_id', $keys);
+        $startIdx = array_search('start_time', $keys);
+        $endIdx   = array_search('end_time', $keys);
+
+        $topDepartId = $user['top_depart_id'];
+        $errors = [];
+        $update_map = [];
+
+        // 1. 档案预加载
+        $empCodes = array_filter(array_unique(array_column($array, $empIdx)));
+        $empModels = Employee::where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('number', $empCodes)->get(['id', 'number', 'title']);
+        $dbEmps = $empModels->pluck('id', 'number')->toArray();
+        $empDisplayMap = $empModels->mapWithKeys(fn($item) => [$item->id => "[{$item->number}]{$item->title}"])->toArray();
+
+        // 2. 日期、日历、主表预查
+        $allDateTs = [];
+        foreach ($array as $row) {
+            list($status, $ts) = $this->convertExcelCellToDate($row[$timeIdx] ?? '');
+            if ($status) $allDateTs[] = strtotime(date('Y-m-d', $ts));
+        }
+        $allDateTs = array_unique($allDateTs);
+        $calendarMap = DB::table('calendar_details')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('time', $allDateTs)->pluck('is_work', 'time')->toArray();
+
+        $existMainOrders = DB::table('p_leave_over_order')
+            ->where('top_depart_id', $topDepartId)
+            ->whereIn('order_time', $allDateTs)
+            ->where('type', PLeaveOverOrder::TYPE_TWO)
+            ->where('del_time', 0)->get()->keyBy('order_time');
+
+        // 3. 工时设置预加载
+        $specialWorkMap = DB::table('employee_work_range')->whereIn('employee_id', array_values($dbEmps))
+            ->where('top_depart_id', $topDepartId)->get()->groupBy('employee_id');
+        $commonWorkRanges = DB::table('work_range_details')->where('del_time', 0)->where('top_depart_id', $topDepartId)->get();
+        $commonPeriods = [];
+        foreach ($commonWorkRanges as $wr) {
+            $commonPeriods[] = ['s' => $wr->start_time_hour * 60 + $wr->start_time_min, 'e' => $wr->end_time_hour * 60 + $wr->end_time_min];
+        }
+
+        // 4. 库内明细预查 (跨单重叠校验)
+        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
+        $dbExistingWork = DB::table('p_leave_over_order_details as d')
+            ->join('p_leave_over_order as m', 'd.main_id', '=', 'm.id')
+            ->where('m.top_depart_id', $topDepartId)
+            ->whereIn('m.order_time', $allDateTs)
+            ->where('m.type', PLeaveOverOrder::TYPE_TWO)
+            ->where('m.del_time', 0)->where('d.del_time', 0)
+            ->whereNotIn('m.code', $allCodes)
+            ->select('m.order_time', 'd.employee_id', 'd.start_time_hour', 'd.start_time_min', 'd.end_time_hour', 'd.end_time_min')
+            ->get()->groupBy(fn($item) => $item->order_time . '_' . $item->employee_id);
+
+        $dbOrders = DB::table('p_leave_over_order')->where('top_depart_id', $topDepartId)
+            ->whereIn('code', $allCodes)->where('del_time', 0)->get()->keyBy('code');
+
+        $internalOverlapMap = [];
+
+        // 5. 全量循环检查
+        foreach ($array as $rowIndex => $row) {
+            $line = $rowIndex + 1;
+            $rowErrors = [];
+
+            list($status, $ts) = $this->convertExcelCellToDate($row[$timeIdx] ?? '');
+            $ts = $status ? strtotime(date('Y-m-d', $ts)) : 0;
+            $array[$rowIndex][$timeIdx] = $ts;
+
+            if (!$ts) { $errors[] = "第{$line}行:日期格式错误"; continue; }
+
+            // A. 单据存在性逻辑
+            $valCode = trim($row[$codeIdx] ?? '');
+            if ($valCode === '') {
+                if (isset($existMainOrders[$ts])) {
+                    $rowErrors[] = date('Y-m-d', $ts) . "已存在加班单[{$existMainOrders[$ts]->code}],请填写单号进行编辑";
+                }
+            } else {
+                if (isset($dbOrders[$valCode])) {
+                    if ($dbOrders[$valCode]->order_time != $ts) $rowErrors[] = "单据[{$valCode}]原日期不符";
+                    $update_map[$rowIndex] = $dbOrders[$valCode]->id;
+                } else { $rowErrors[] = "单据号[{$valCode}]不存在"; }
+            }
+
+            // B. 人员校验
+            $empId = $dbEmps[trim($row[$empIdx])] ?? 0;
+            $empName = $empDisplayMap[$empId] ?? "工号:".$row[$empIdx];
+            if (!$empId) {
+                $errors[] = "第{$line}行:人员不存在"; continue;
+            }
+
+            // C. 时间格式校验
+            list($sH, $sM) = $this->parseTimeHourMin($row[$startIdx] ?? '');
+            list($eH, $eM) = $this->parseTimeHourMin($row[$endIdx] ?? '');
+            $curS = $sH * 60 + $sM; $curE = $eH * 60 + $eM;
+
+            if ($curS >= $curE) {
+                $rowErrors[] = "结束时间需晚于开始时间";
+            } else {
+                // D. 加班规则校验 (核心逻辑)
+                $isWorkDay = (int)($calendarMap[$ts] ?? 0) === 1;
+
+                if ($isWorkDay) {
+                    // 如果是工作日,加班不能在工作时间内
+                    $empPeriods = isset($specialWorkMap[$empId]) ? [] : $commonPeriods;
+                    if (isset($specialWorkMap[$empId])) {
+                        foreach ($specialWorkMap[$empId] as $sw) {
+                            $empPeriods[] = ['s' => $sw->start_time_hour * 60 + $sw->start_time_min, 'e' => $sw->end_time_hour * 60 + $sw->end_time_min];
+                        }
+                    }
+                    foreach ($empPeriods as $p) {
+                        if ($curS < $p['e'] && $p['s'] < $curE) {
+                            $rowErrors[] = "加班时间不能与正常工作时间重叠";
+                            break;
+                        }
+                    }
+                }
+
+                // E. 重叠校验
+                $checkKey = $ts . "_" . $empId;
+                // 表内重叠
+                if (isset($internalOverlapMap[$checkKey])) {
+                    foreach ($internalOverlapMap[$checkKey] as $p) {
+                        if ($curS < $p['e'] && $p['s'] < $curE) { $rowErrors[] = "在表内存在时间重叠"; break; }
+                    }
+                }
+                // 库内重叠
+                if (isset($dbExistingWork[$checkKey])) {
+                    foreach ($dbExistingWork[$checkKey] as $p) {
+                        $exS = $p->start_time_hour * 60 + $p->start_time_min; $exE = $p->end_time_hour * 60 + $p->end_time_min;
+                        if ($curS < $exE && $exS < $curE) { $rowErrors[] = "与已有的加班单时间重叠"; break; }
+                    }
+                }
+            }
+
+            if (!empty($rowErrors)) {
+                $errors[] = "第{$line}行:{$empName} " . implode(',', $rowErrors);
+            } else {
+                $internalOverlapMap[$ts . "_" . $empId][] = ['s' => $curS, 'e' => $curE];
+            }
+        }
+
+        return [!empty($errors) ? implode('|', $errors) : "", $update_map, $dbEmps];
+    }
+
     /**
      * 解析并校验时间
      */

+ 464 - 0
app/Service/PLeaveOverService.php

@@ -0,0 +1,464 @@
+<?php
+
+namespace App\Service;
+
+use App\Model\CalendarDetails;
+use App\Model\PLeaveOverOrder;
+use App\Model\PLeaveOverOrderDetails;
+use App\Model\Employee;
+use App\Model\Item;
+use App\Model\WorkRangeDetails;
+use Illuminate\Support\Facades\DB;
+
+class PLeaveOverService extends Service
+{
+    public function pLeaveOverEdit($data,$user){
+        list($status,$msg) = $this->pLeaveOverRule($data, $user, false);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = PLeaveOverOrder::where('id',$data['id'])->first();
+            $model->save();
+
+            $time = time();
+            PLeaveOverOrderDetails::where('del_time',0)
+                ->where('main_id', $model->id)
+                ->update(['del_time' => $time]);
+            $this->saveDetail($model->id, $time, $data);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    public function pLeaveOverAdd($data,$user){
+        list($status,$msg) = $this->pLeaveOverRule($data, $user);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = new PLeaveOverOrder();
+            $model->code = $this->generateBillNo([
+                'top_depart_id' => $user['top_depart_id'],
+                'type' => PLeaveOverOrder::Order_type,
+                'period' => date("Ym", $data['order_time'])
+            ]);
+            $model->order_time = $data['order_time'] ?? 0;
+            $model->type = $data['type'];
+            $model->crt_id = $user['id'];
+            $model->top_depart_id = $data['top_depart_id'];
+            $model->save();
+
+            $this->saveDetail($model->id, time(), $data);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    private function saveDetail($id, $time, $data){
+        if(! empty($data['details'])){
+            $unit = [];
+            foreach ($data['details'] as $value){
+                $unit[] = [
+                    'main_id' => $id,
+                    'employee_id' => $value['employee_id'],
+                    'start_time_hour' => $value['start_time_hour'],
+                    'start_time_min' => $value['start_time_min'],
+                    'end_time_hour' => $value['end_time_hour'],
+                    'end_time_min' => $value['end_time_min'],
+                    'total_min' => $value['total_min'],
+                    'crt_time' => $time,
+                    'top_depart_id' => $value['top_depart_id'],
+                    'type' => $value['type'],
+                ];
+            }
+            if(! empty($unit)) PLeaveOverOrderDetails::insert($unit);
+        }
+    }
+
+    private function getDetail($id){
+        $data = PLeaveOverOrderDetails::where('del_time',0)
+            ->where('main_id', $id)
+            ->select('employee_id', 'start_time_hour', 'start_time_min', 'end_time_hour', 'end_time_min', 'total_min')
+            ->get()->toArray();
+
+        $id = array_column($data,'employee_id');
+        $map = Employee::whereIn('id', $id)
+            ->select('title','id','number')
+            ->get()
+            ->keyBy('id')
+            ->toArray();
+
+        foreach ($data as $key => $value){
+            $tmp = $map[$value['employee_id']] ?? [];
+            $merge = [];
+            $merge['employee_title'] = $tmp['title'];
+            $merge['employee_number'] = $tmp['number'];
+            $data[$key] = array_merge($value, $merge);
+        }
+
+        $detail = [
+            'details' => $data,
+        ];
+
+        foreach ($detail as $key => $value) {
+            if (empty($value)) {
+                $detail[$key] = (object)[]; // 转成 stdClass 对象
+            }
+        }
+
+        return $detail;
+    }
+
+    public function pLeaveOverDel($data){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+
+        try {
+            DB::beginTransaction();
+            $time = time();
+
+            PLeaveOverOrder::where('del_time',0)
+                ->whereIn('id',$data['id'])
+                ->update(['del_time' => $time]);
+
+            PLeaveOverOrderDetails::where('del_time',0)
+                ->whereIn('main_id', $data['id'])
+                ->update(['del_time' => $time]);
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    public function pLeaveOverDetail($data, $user){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+        $customer = PLeaveOverOrder::where('del_time',0)
+            ->where('id',$data['id'])
+            ->first();
+        if(empty($customer)) return [false,'单据不存在或已被删除'];
+        $customer = $customer->toArray();
+        $customer['type_title'] = PLeaveOverOrder::Type[$customer['type']] ?? "";
+        $customer['crt_name'] = Employee::where('id',$customer['crt_id'])->value('title');
+        $customer['crt_time'] = $customer['crt_time'] ? date("Y-m-d H:i:s",$customer['crt_time']): '';
+        $customer['order_time'] = $customer['order_time'] ? date("Y-m-d",$customer['order_time']): '';
+
+        $details = $this->getDetail($data['id']);
+        $customer = array_merge($customer, $details);
+
+        return [true, $customer];
+    }
+
+    public function pLeaveOverCommon($data,$user, $field = []){
+        if(empty($field)) $field = PLeaveOverOrder::$field;
+
+        $model = PLeaveOverOrder::Clear($user,$data);
+        $model = $model->where('del_time',0)
+            ->select($field)
+            ->orderby('id', 'desc');
+
+        if(! empty($data['code'])) $model->where('code', 'LIKE', '%'.$data['code'].'%');
+        if(! empty($data['type'])) $model->where('type', $data['type']);
+        if(! empty($data['id'])) $model->whereIn('id', $data['id']);
+        if(! empty($data['crt_time'][0]) && ! empty($data['crt_time'][1])) {
+            $return = $this->changeDateToTimeStampAboutRange($data['crt_time']);
+            $model->where('crt_time','>=',$return[0]);
+            $model->where('crt_time','<=',$return[1]);
+        }
+
+        return $model;
+    }
+
+    public function pLeaveOverList($data,$user){
+        $model = $this->pLeaveOverCommon($data, $user);
+        $list = $this->limit($model,'',$data);
+        $list = $this->fillData($list);
+
+        return [true, $list];
+    }
+
+    public function pLeaveOverRule(&$data, $user, $is_add = true)
+    {
+        $data['top_depart_id'] = $user['top_depart_id'];
+        $topDepartId = $data['top_depart_id'];
+
+        // 1. 基础校验
+        if (empty($data['type']) || ! isset(PLeaveOverOrder::Type[$data['type']])) return [false, '单据类型非法'];
+        $typeName = PLeaveOverOrder::Type[$data['type']];
+        if (empty($data['order_time'])) return [false, '单据日期不能为空'];
+        $data['order_time'] = $this->changeDateToDate($data['order_time']);
+        $orderTime = $data['order_time'];
+        $orderType = $data['type'];
+
+        if (!$is_add) {
+            if (empty($data['id'])) return [false, 'ID不能为空'];
+            $orderExists = DB::table('p_leave_over_order')
+                ->where('id', $data['id'])
+                ->where('top_depart_id', $topDepartId)
+                ->where('del_time', 0)
+                ->exists();
+            if (!$orderExists) return [false, '原单据不存在或已被删除'];
+        }
+
+        // --- 日历工作日校验 ---
+        $calendar = DB::table('calendar_details')
+            ->where('del_time', 0)
+            ->where('top_depart_id', $topDepartId)
+            ->where('time', $orderTime)
+            ->first();
+        if (!$calendar) return [false, '所选日期未在公司日历中设置,无法校验'];
+
+        $isWorkDay = (int)$calendar->is_work === CalendarDetails::TYPE_ONE;
+
+        if (!$isWorkDay && $orderType == PLeaveOverOrder::TYPE_ONE) {
+            return [false, date("Y-m-d", $orderTime) . "为非工作日,无需提交请假单"];
+        }
+
+        // 2. 获取【通用】工作时间段
+        $commonWorkPeriods = [];
+        if ($isWorkDay) {
+            $workRanges = DB::table('work_range_details')
+                ->where('del_time', 0)
+                ->where('top_depart_id', $topDepartId)
+                ->get();
+            foreach ($workRanges as $wr) {
+                $commonWorkPeriods[] = [
+                    's' => $wr->start_time_hour * 60 + $wr->start_time_min,
+                    'e' => $wr->end_time_hour * 60 + $wr->end_time_min
+                ];
+            }
+        }
+
+        // 3. 预加载明细中所有人员的信息及【个性化】工时
+        if (empty($data['details'])) return [false, '明细不能为空'];
+        $allEmpIds = array_filter(array_unique(array_column($data['details'], 'employee_id')));
+
+        $empDisplayMap = Employee::whereIn('id', $allEmpIds)
+            ->get(['id', 'number', 'title'])
+            ->mapWithKeys(fn($item) => [$item->id => "[{$item->number}]{$item->title}"])->toArray();
+
+        // --- 新增:预加载个性化工时 ---
+        $specialWorkMap = DB::table('employee_work_range')
+            ->whereIn('employee_id', $allEmpIds)
+            ->where('top_depart_id', $topDepartId)
+            ->get()
+            ->groupBy('employee_id');
+
+        // 预查库内已有时段 (保持不变)
+        $dbExistingDetails = DB::table('p_leave_over_order_details as d')
+            ->join('p_leave_over_order as m', 'd.main_id', '=', 'm.id')
+            ->where('m.top_depart_id', $topDepartId)
+            ->where('m.order_time', $orderTime)
+            ->where('m.del_time', 0)
+            ->where('d.del_time', 0)
+            ->whereIn('d.employee_id', $allEmpIds)
+            ->when(!$is_add, fn($q) => $q->where('m.id', '<>', $data['id']))
+            ->select('d.employee_id', 'd.start_time_hour', 'd.start_time_min', 'd.end_time_hour', 'd.end_time_min', 'm.type as order_type')
+            ->get()
+            ->groupBy('employee_id');
+
+        // 4. 循环明细校验
+        $internalOverlap = [];
+
+        foreach ($data['details'] as $key => &$value) {
+            $empId = $value['employee_id'] ?? 0;
+            $empName = $empDisplayMap[$empId] ?? "";
+            if(empty($empName)) return [false, '人员不存在或已被删除'];
+
+            // A. 时间合法性校验
+            if ($value['start_time_hour'] > 23 || $value['end_time_hour'] > 24 || $value['start_time_min'] > 59 || $value['end_time_min'] > 59) {
+                return [false, "人员{$empName}:填写的时间段不合法"];
+            }
+            $currentStart = $value['start_time_hour'] * 60 + $value['start_time_min'];
+            $currentEnd   = $value['end_time_hour'] * 60 + $value['end_time_min'];
+            if ($currentStart >= $currentEnd) return [false, "人员{$empName}:开始时间必须早于结束时间"];
+
+            $value['total_min'] = $currentEnd - $currentStart;
+            $value['type'] = $orderType;
+
+            // B. 工作时间逻辑校验 (重点:确定使用哪套工时标准)
+            if ($isWorkDay) {
+                // 判定:是个性化还是通用?
+                $currentEmpPeriods = [];
+                if (isset($specialWorkMap[$empId])) {
+                    foreach ($specialWorkMap[$empId] as $sw) {
+                        $currentEmpPeriods[] = [
+                            's' => $sw->start_time_hour * 60 + $sw->start_time_min,
+                            'e' => $sw->end_time_hour * 60 + $sw->end_time_min
+                        ];
+                    }
+                } else {
+                    $currentEmpPeriods = $commonWorkPeriods;
+                }
+
+                if (empty($currentEmpPeriods)) return [false, "人员{$empName}:未找到对应的工作时间设置,无法校验"];
+
+                if ($orderType == PLeaveOverOrder::TYPE_ONE) {
+                    // 请假:必须在工作时间内
+                    $inWorkRange = false;
+                    foreach ($currentEmpPeriods as $wp) {
+                        if ($currentStart >= $wp['s'] && $currentEnd <= $wp['e']) {
+                            $inWorkRange = true;
+                            break;
+                        }
+                    }
+                    if (!$inWorkRange) return [false, "人员{$empName}:请假时间必须在对应的工作时间段内"];
+                } else {
+                    // 加班:不能在工作时间段内
+                    foreach ($currentEmpPeriods as $wp) {
+                        if ($currentStart < $wp['e'] && $wp['s'] < $currentEnd) {
+                            return [false, "人员{$empName}:加班时间与正常工作时间重叠"];
+                        }
+                    }
+                }
+            }
+
+            // C. 内部重叠校验
+            if (isset($internalOverlap[$empId])) {
+                foreach ($internalOverlap[$empId] as $period) {
+                    if ($currentStart < $period['e'] && $period['s'] < $currentEnd) {
+                        return [false, "人员{$empName}:本次提交的数据中存在时间重叠"];
+                    }
+                }
+            }
+            $internalOverlap[$empId][] = ['s' => $currentStart, 'e' => $currentEnd];
+
+            // D. 库内同类型重叠校验
+            if (isset($dbExistingDetails[$empId])) {
+                foreach ($dbExistingDetails[$empId] as $dbD) {
+                    if ($dbD->order_type == $orderType) {
+                        $exStart = $dbD->start_time_hour * 60 + $dbD->start_time_min;
+                        $exEnd   = $dbD->end_time_hour * 60 + $dbD->end_time_min;
+                        if ($currentStart < $exEnd && $exStart < $currentEnd) {
+                            $typeStr = $orderType == PLeaveOverOrder::TYPE_ONE ? "请假" : "加班";
+                            return [false, "人员{$empName}:该时段与已有的{$typeStr}记录重叠"];
+                        }
+                    }
+                }
+            }
+
+            $value['top_depart_id'] = $topDepartId;
+        }
+
+        // 5. 唯一性校验
+        $uniqueCheck = PLeaveOverOrder::where('top_depart_id', $topDepartId)
+            ->where('order_time', $orderTime)
+            ->where('type', $orderType)
+            ->where('del_time', 0);
+        if (! $is_add) $uniqueCheck->where('id', '<>', $data['id']);
+
+        if ($uniqueCheck->exists()) {
+            return [false, date("Y-m-d", $orderTime) . "已存在{$typeName},请在原单据上修改"];
+        }
+
+        return [true, ''];
+    }
+
+    public function fillData($data){
+        if(empty($data['data'])) return $data;
+
+        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_column($data['data'],'crt_id')));
+        foreach ($data['data'] as $key => $value){
+            $data['data'][$key]['crt_time'] = $value['crt_time'] ? date('Y-m-d H:i:s',$value['crt_time']) : '';
+            $data['data'][$key]['order_time'] = $value['order_time'] ? date('Y-m-d',$value['order_time']) : '';
+            $data['data'][$key]['crt_name'] = $emp[$value['crt_id']] ?? '';
+            $data['data'][$key]['type_title'] = PLeaveOverOrder::Type[$value['type']] ?? "";
+        }
+
+        return $data;
+    }
+
+    public function fillDataForExportDaily($data, $column, &$return)
+    {
+        if (empty($data)) return;
+
+        $mainIds = array_column($data, 'id');
+        // 1. 获取子表详情及关联档案映射
+        $detailsMap = $this->getLeaveOverDetailsMap($mainIds);
+
+        foreach ($data as $main) {
+            $mainId = $main['id'];
+            $details = $detailsMap[$mainId] ?? [];
+
+            // 2. 提取并格式化主表共有信息
+            $mainInfo = [
+                'code'       => $main['code'],
+                'order_time' => !empty($main['order_time']) ? date('Y-m-d', $main['order_time']) : '',
+            ];
+
+            if (empty($details)) {
+                // 如果主表没有明细,依然根据 column 导出一行(包含主表信息,明细列为空)
+                $tempRow = [];
+                foreach ($column as $col) {
+                    $tempRow[] = $mainInfo[$col] ?? '';
+                }
+                $return[] = $tempRow;
+            } else {
+                // 3. 平铺导出:每一行明细都带上主表的 code 和日期
+                foreach ($details as $sub) {
+                    $fullRowData = array_merge($mainInfo, $sub);
+                    $tempRow = [];
+                    foreach ($column as $col) {
+                        // $col 对应的是配置里的 export 字段,如 'employee_number', 'start_time' 等
+                        $tempRow[] = $fullRowData[$col] ?? '';
+                    }
+                    $return[] = $tempRow;
+                }
+            }
+        }
+    }
+
+    public function getLeaveOverDetailsMap($mainIds)
+    {
+        // 1. 获取所有子表记录
+        $details = DB::table('p_leave_over_order_details')
+            ->where('del_time', 0)
+            ->whereIn('main_id', $mainIds)
+            ->get();
+
+        if ($details->isEmpty()) return [];
+
+        // 2. 批量获取人员档案映射
+        $empIds = $details->pluck('employee_id')->unique()->toArray();
+        $empMap = DB::table('employee')
+            ->whereIn('id', $empIds)
+            ->get(['id', 'title', 'number'])
+            ->keyBy('id');
+
+        $res = [];
+        foreach ($details as $item) {
+            $emp = $empMap[$item->employee_id] ?? null;
+
+            // 3. 组装明细行数据(Key 必须与 header 配置中的 export 字段一致)
+            $detailRow = [
+                // 人员信息
+                'employee_number' => $emp ? $emp->number : '',
+                'employee_title'  => $emp ? $emp->title : '',
+                // 时间段格式化 8:0 -> 08:00
+                'start_time'      => sprintf('%02d:%02d', $item->start_time_hour, $item->start_time_min),
+                'end_time'        => sprintf('%02d:%02d', $item->end_time_hour, $item->end_time_min),
+                // 时长(分钟)
+                'total_min'       => $item->total_min,
+            ];
+
+            $res[$item->main_id][] = $detailRow;
+        }
+
+        return $res;
+    }
+}

+ 15 - 12
app/Service/PersonWorkService.php

@@ -79,8 +79,6 @@ class PersonWorkService extends Service
                     'rd_total_days' => $value['rd_total_days'],
                     'total_hours' => $value['total_hours'],
                     'rd_total_hours' => $value['rd_total_hours'],
-                    'start_time' => $value['start_time'],
-                    'end_time' => $value['end_time'],
                     'crt_time' => $time,
                     'top_depart_id' => $value['top_depart_id'],
                 ];
@@ -92,7 +90,7 @@ class PersonWorkService extends Service
     private function getDetail($id){
         $data = MonthlyPwOrderDetails::where('del_time',0)
             ->where('main_id', $id)
-            ->select('employee_id', 'total_days', 'rd_total_days', 'total_hours', 'rd_total_hours', 'start_time', 'end_time')
+            ->select('employee_id', 'total_days', 'rd_total_days', 'total_hours', 'rd_total_hours')
             ->get()->toArray();
 
         $id = array_column($data,'employee_id');
@@ -104,8 +102,7 @@ class PersonWorkService extends Service
             $merge = [];
             $merge['employee_title'] = $tmp['title'];
             $merge['employee_number'] = $tmp['number'];
-            $merge['start_time'] = date("Y-m-d", $value['start_time']);
-            $merge['end_time'] = date("Y-m-d", $value['end_time']);
+
             $data[$key] = array_merge($value, $merge);
         }
 
@@ -207,10 +204,6 @@ class PersonWorkService extends Service
             $res = $this->checkNumber($value['rd_total_hours'],2,'non-negative');
             if(! $res['valid']) return [false,'研发总工时:' . $res['error']];
 
-            if(empty($value['start_time'])) return [false, '开始时间不能为空'];
-            $data['details'][$key]['start_time'] = $this->changeDateToDate($value['start_time']);
-            if(empty($value['end_time'])) return [false, '结束时间不能为空'];
-            $data['details'][$key]['end_time'] = $this->changeDateToDate($value['end_time']);
             $data['details'][$key]['top_depart_id'] = $data['top_depart_id'];
         }
         list($status, $msg) = $this->checkArrayRepeat($data['details'],'employee_id','人员');
@@ -306,8 +299,6 @@ class PersonWorkService extends Service
                 'rd_total_days'  => $item->rd_total_days,
                 'total_hours'    => $item->total_hours,
                 'rd_total_hours' => $item->rd_total_hours,
-                'start_time'     => $item->start_time ? date("Y-m-d", $item->start_time) : '',
-                'end_time'       => $item->end_time ? date("Y-m-d", $item->end_time) : '',
             ];
         }
         return $res; // 返回 [main_id => [detail_row, detail_row]]
@@ -528,15 +519,18 @@ class PersonWorkService extends Service
 
             $empName = $empDisplayMap[$empId] ?? "ID:{$empId}";
 
-            // 校验数字有效性 (修正为使用 $value)
             $res = $this->checkNumber($value['start_time_hour'], 0, 'non-negative');
             if(!$res['valid']) return [false, "人员{$empName}开始点:" . $res['error']];
+            if($value['start_time_hour'] > 23) return [false, false, "人员{$empName}开始点不合法"];
             $res = $this->checkNumber($value['start_time_min'], 0, 'non-negative');
             if(!$res['valid']) return [false, "人员{$empName}开始分:" . $res['error']];
+            if($value['start_time_min'] > 60) return [false, false, "人员{$empName}开始点不合法"];
             $res = $this->checkNumber($value['end_time_hour'], 0, 'non-negative');
             if(!$res['valid']) return [false, "人员{$empName}结束点:" . $res['error']];
+            if($value['end_time_hour'] > 24) return [false, false, "人员{$empName}结束点不合法"];
             $res = $this->checkNumber($value['end_time_min'], 0, 'non-negative');
             if(!$res['valid']) return [false, "人员{$empName}结束分:" . $res['error']];
+            if($value['end_time_min'] > 60) return [false, false, "人员{$empName}结束分不合法"];
 
             $currentStart = $value['start_time_hour'] * 60 + $value['start_time_min'];
             $currentEnd   = $value['end_time_hour'] * 60 + $value['end_time_min'];
@@ -713,4 +707,13 @@ class PersonWorkService extends Service
 
         return $res;
     }
+
+    public function dailyPwOrderCreate($data,$user){
+        $data['top_depart_id'] = $user['top_depart_id'];
+        if(empty($data['month'])) return [false, '月份不能为空'];
+
+        $monthStart = $this->changeDateToDate($data['month']);
+        $data['month'] = $monthStart;
+        $monthEnd = strtotime('+1 month', $monthStart) - 1;
+    }
 }

+ 72 - 0
config/excel/leaveOrder.php

@@ -0,0 +1,72 @@
+<?php
+return [
+    "name" => "请假单",
+    "array" => [
+        [
+            'key' =>'code',
+            'export' =>'code',
+            'value' => '单据编码',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '(如果填写则是编辑)'
+        ],
+        [
+            'key' =>'order_time',
+            'export' =>'order_time',
+            'value' => '请假日期',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(2026-02-01)'
+        ],
+        [
+            'key' =>'employee_id',
+            'export' =>'employee_number',
+            'value' => '人员工号',
+            'required' => true,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(填写人员工号)'
+        ],
+        [
+            'key' =>'employee_title',
+            'export' =>'employee_title',
+            'value' => '人员名称',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
+        [
+            'key' =>'start_time',
+            'export' =>'start_time',
+            'value' => '开始时间段',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(如 8:00)'
+        ],
+        [
+            'key' =>'end_time',
+            'export' =>'end_time',
+            'value' => '结束时间段',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(如 9:00)'
+        ],
+    ]
+];

+ 0 - 22
config/excel/monthPwOrder.php

@@ -90,27 +90,5 @@ return [
             'enums' => [],
             'comments' => '必填'
         ],
-        [
-            'key' =>'start_time',
-            'export' =>'start_time',
-            'value' => '开始日期',
-            'required' => true,
-            'is_main' => false,
-            'default' => "",
-            'unique' => false,
-            'enums' => [],
-            'comments' => '必填'
-        ],
-        [
-            'key' =>'end_time',
-            'export' =>'end_time',
-            'value' => '结束日期',
-            'required' => true,
-            'is_main' => false,
-            'unique' => false,
-            'enums' => [],
-            'default' => "",
-            'comments' => ""
-        ],
     ]
 ];

+ 72 - 0
config/excel/overtimeOrder.php

@@ -0,0 +1,72 @@
+<?php
+return [
+    "name" => "加班单",
+    "array" => [
+        [
+            'key' =>'code',
+            'export' =>'code',
+            'value' => '单据编码',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '(如果填写则是编辑)'
+        ],
+        [
+            'key' =>'order_time',
+            'export' =>'order_time',
+            'value' => '加班日期',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(2026-02-01)'
+        ],
+        [
+            'key' =>'employee_id',
+            'export' =>'employee_number',
+            'value' => '人员工号',
+            'required' => true,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(填写人员工号)'
+        ],
+        [
+            'key' =>'employee_title',
+            'export' =>'employee_title',
+            'value' => '人员名称',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
+        [
+            'key' =>'start_time',
+            'export' =>'start_time',
+            'value' => '开始时间段',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(如 8:00)'
+        ],
+        [
+            'key' =>'end_time',
+            'export' =>'end_time',
+            'value' => '结束时间段',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(如 9:00)'
+        ],
+    ]
+];

+ 17 - 0
routes/api.php

@@ -132,6 +132,7 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('dailyPwOrderAdd', 'Api\PersonWorkController@dailyPwOrderAdd');
     $route->any('dailyPwOrderDel', 'Api\PersonWorkController@dailyPwOrderDel');
     $route->any('dailyPwOrderDetail', 'Api\PersonWorkController@dailyPwOrderDetail');
+    $route->any('dailyPwOrderCreate', 'Api\PersonWorkController@dailyPwOrderCreate');
 
     //设备日工时单
     $route->any('dailyDwOrderList', 'Api\DeviceWorkController@dailyDwOrderList');
@@ -139,5 +140,21 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('dailyDwOrderAdd', 'Api\DeviceWorkController@dailyDwOrderAdd');
     $route->any('dailyDwOrderDel', 'Api\DeviceWorkController@dailyDwOrderDel');
     $route->any('dailyDwOrderDetail', 'Api\DeviceWorkController@dailyDwOrderDetail');
+
+    //日历设置
+    $route->any('calendarList', 'Api\CalendarController@calendarList');
+    $route->any('calendarEdit', 'Api\CalendarController@calendarEdit');
+    $route->any('calendarAdd', 'Api\CalendarController@calendarAdd');
+    $route->any('calendarDel', 'Api\CalendarController@calendarDel');
+    $route->any('calendarDetail', 'Api\CalendarController@calendarDetail');
+
+    //工时设置 暂时后台写死数据 toDo
+
+    //请假单 加班单
+    $route->any('pLeaveOverOrderList', 'Api\PLeaveOverController@pLeaveOverList');
+    $route->any('pLeaveOverEdit', 'Api\PLeaveOverController@pLeaveOverEdit');
+    $route->any('pLeaveOverAdd', 'Api\PLeaveOverController@pLeaveOverAdd');
+    $route->any('pLeaveOverDel', 'Api\PLeaveOverController@pLeaveOverDel');
+    $route->any('pLeaveOverDetail', 'Api\PLeaveOverController@pLeaveOverDetail');
 });