Procházet zdrojové kódy

Merge remote-tracking branch 'origin/master'

gogs před 3 týdny
rodič
revize
b683973341

+ 91 - 0
app/Http/Controllers/Api/ItemController.php

@@ -299,6 +299,19 @@ class ItemController extends BaseController
         }
     }
 
+    public function itemNodeMissionListBySearch(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemNodeMissionListBySearch($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
     public function itemNodeMissionDetail(Request $request)
     {
         $service = new ItemService();
@@ -311,4 +324,82 @@ class ItemController extends BaseController
             return $this->json_return(201,$data);
         }
     }
+
+    public function itemNodeMissionDetailBoard(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemNodeMissionDetailBoard($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function itemNodeMissionUpdateProgress(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemNodeMissionUpdateProgress($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function itemNodeMissionUpdateProgressContent(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemNodeMissionUpdateProgressContent($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function itemNodeMissionUpdateState(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemNodeMissionUpdateState($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function itemGannetList(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemGannetList($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function itemGannetGetSonData(Request $request)
+    {
+        $service = new ItemService();
+        $user = $request->userData;
+        list($status,$data) = $service->itemGannetGetSonData($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
 }

+ 115 - 0
app/Http/Controllers/Api/WorkFlowController.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Service\WorkFlowService;
+use Illuminate\Http\Request;
+
+class WorkFlowController extends BaseController
+{
+    public function workFlowEdit(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->workFlowEdit($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function workFlowAdd(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->workFlowAdd($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function workFlowDel(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->workFlowDel($request->all(), $user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+
+    }
+
+    public function workFlowList(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->workFlowList($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function workFlowDetail(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->workFlowDetail($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function getMyPendingApprovals(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->getMyPendingApprovals($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function getMyHandledApprovals(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->getMyHandledApprovals($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+
+    public function approval(Request $request)
+    {
+        $service = new WorkFlowService();
+        $user = $request->userData;
+        list($status,$data) = $service->approval($request->all(),$user);
+
+        if($status){
+            return $this->json_return(200,'',$data);
+        }else{
+            return $this->json_return(201,$data);
+        }
+    }
+}

+ 1 - 1
app/Model/DataScopeBaseModel.php

@@ -115,7 +115,7 @@ class DataScopeBaseModel extends Model
                         ->whereColumn($relationTable . ".{$relationTableId}", $alias . '.id') // 关联主表的 ID
                         ->where($relationTable . '.data_id', $user['id'])         // 过滤当前操作人
                         ->where($relationTable . '.del_time', 0)                 // 排除已删除的关联记录
-                        ->where($relationTable . '.top_depart_id', 0);
+                        ->where($relationTable . '.top_depart_id', $user['top_depart_id']);
                 });
             }
         }

+ 24 - 0
app/Model/Draft.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+//草稿
+class Draft extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "draft"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    public static $field = ['id', 'type', 'document_id', 'crt_id', 'crt_time', 'upd_time'];
+
+    /**
+     * 属性转换为原生类型
+     */
+    protected $casts = [
+        'content' => 'array', // 声明 content 字段读取时自动转为数组
+        'opt_user' => 'array', // 声明 content 字段读取时自动转为数组
+    ];
+}

+ 2 - 2
app/Model/Item.php

@@ -14,11 +14,11 @@ class Item extends DataScopeBaseModel
     const table_column = "item_employee";
     const table_id_column = "item_id";
 
-    public static $field = ['title','id','code','start_time','end_time','mark','crt_id','crt_time','state','budget','charge_id','item_attribute','field','is_review_required','review_id','priority_id'];
+    public static $field = ['title','id','code','start_time','end_time','mark','crt_id','crt_time','state','budget','charge_id','item_attribute','field','is_review_required','review_id','priority_id', 'approval_state','progress'];
     public static $report_field_1 = ['title','id','code','start_time','end_time','mark','budget','field'];
 
-    const TYPE_MINUS_TWO = -2;
     const TYPE_MINUS_ONE = -1;
+    const TYPE_MINUS_TWO = -2;
     const TYPE_ONE = 1;
     const TYPE_TWO = 2;
     const TYPE_THREE = 3;

+ 1 - 1
app/Model/ItemNode.php

@@ -17,8 +17,8 @@ class ItemNode extends DataScopeBaseModel
 
     public static $field = ['*'];
 
-    const TYPE_MINUS_TWO = -2;
     const TYPE_MINUS_ONE = -1;
+    const TYPE_MINUS_TWO = -2;
     const TYPE_ONE = 1;
     const TYPE_TWO = 2;
     const TYPE_THREE = 3;

+ 3 - 0
app/Model/ItemNodeEmployee.php

@@ -9,4 +9,7 @@ class ItemNodeEmployee extends DataScopeBaseModel
     const CREATED_AT = 'crt_time';
     const UPDATED_AT = 'upd_time';
     protected $dateFormat = 'U';
+
+    const type_zero = 0; // 节点负责人
+    const type_one = 1; // 项目负责人
 }

+ 12 - 0
app/Model/ItemNodeMissionContent.php

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

+ 4 - 0
app/Model/ItemNodeMissionEmployee.php

@@ -9,4 +9,8 @@ class ItemNodeMissionEmployee extends DataScopeBaseModel
     const CREATED_AT = 'crt_time';
     const UPDATED_AT = 'upd_time';
     protected $dateFormat = 'U';
+
+    const type_zero = 0; // 任务负责人
+    const type_one = 1; // 项目负责人
+    const type_two = 2; // 节点负责人
 }

+ 32 - 0
app/Model/WorkFlowInstances.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+//流程实例表
+class WorkFlowInstances extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "workflow_instances"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    public static $field = ['id', 'template_id', 'document_id', 'document_type', 'status', 'crt_id', 'crt_time', 'upd_time'];
+
+    const status_one = 'pending';
+    const status_two = 'processing';
+    const status_three = 'approved';
+    const status_four = 'rejected';
+
+    /**
+     * 定义与模板表的关联
+     */
+    public function template()
+    {
+        // 参数1:关联的模型类名
+        // 参数2:当前表(instances)中用来关联的字段名
+        // 参数3:目标表(templates)的主键名
+        return $this->belongsTo(WorkFlowTemplates::class, 'template_id', 'id');
+    }
+}

+ 34 - 0
app/Model/WorkFlowInstancesNodes.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+//流程节点实例表
+class WorkFlowInstancesNodes extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "workflow_instance_nodes"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    public static $field = ['id', 'instance_id', 'node_key', 'label', 'prev_node_key', 'next_node_key', 'assignees', 'approval_type', 'status', 'handled_time', 'crt_time', 'upd_time', 'crt_id'];
+
+    protected $casts = [
+        'assignees' => 'array',
+    ];
+
+    const approval_type_one = 1;
+    const approval_type_two = 2;
+    const approval_type = [
+        self::approval_type_one => '会签',
+        self::approval_type_two => '或签',
+    ];
+
+    const pass_type_one = 1;
+    const pass_type_two = 2;
+    const is_pass_title = [
+        self::pass_type_one => '通过',
+        self::pass_type_two => '驳回',
+    ];
+}

+ 23 - 0
app/Model/WorkFlowTemplates.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+//审批流模板表
+class WorkFlowTemplates extends DataScopeBaseModel
+{
+    protected $guarded = [];
+    protected $table = "workflow_templates"; //指定表
+    const CREATED_AT = 'crt_time';
+    const UPDATED_AT = 'upd_time';
+    protected $dateFormat = 'U';
+    public static $field = ['id', 'code', 'title', 'crt_id', 'crt_time', 'upd_time'];
+
+    /**
+     * 属性转换为原生类型
+     */
+    protected $casts = [
+        'content' => 'array', // 声明 content 字段读取时自动转为数组
+    ];
+}

+ 237 - 1
app/Service/DeviceWorkService.php

@@ -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')
@@ -1220,6 +1220,242 @@ class DeviceWorkService extends Service
         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)
     {
         $list = $data['list'] ?? [];

+ 15 - 5
app/Service/EmployeeService.php

@@ -155,9 +155,14 @@ class EmployeeService extends Service
                 EmployeeDepartPermission::insert($insert);
             }
 
-            EmployeeRole::where('employee_id',$data['id'])->update([
-                'del_time' => $time
-            ]);
+            EmployeeRole::join('role', 'role.id', '=', 'employee_role.role_id')
+                ->where('employee_role.employee_id', $data['id'])
+                ->where('role.tree_type', $user['select_tree_type'])
+                ->where('employee_role.del_time', 0)
+                ->update([
+                    'employee_role.del_time' => $time
+                ]);
+
             if(isset($data['role'])){
                 $insert = [];
                 foreach ($data['role'] as $value){
@@ -430,7 +435,7 @@ class EmployeeService extends Service
 
         // 获取员工ID并查询扩展数据
         $employee_ids = array_column($data['data'], 'id');
-        list($status, $extraMap) = $this->getEmployee($employee_ids);
+        list($status, $extraMap) = $this->getEmployee($employee_ids, $user);
 
         $map = [];
         if(isset($ergs['search_for_month_work'])) list($status, $map) = $this->getEmployeesMonthStats($employee_ids, $ergs['search_for_month_work'], $user);
@@ -561,7 +566,7 @@ class EmployeeService extends Service
         return [true, $result];
     }
 
-    public function getEmployee(array $employee_ids)
+    public function getEmployee(array $employee_ids, $user)
     {
         if (empty($employee_ids)) return [false, []];
 
@@ -570,6 +575,7 @@ class EmployeeService extends Service
             ->join('role as b', 'a.role_id', '=', 'b.id')
             ->where('a.del_time', 0)
             ->where('b.del_time', 0)
+            ->where('b.tree_type', $user['select_tree_type'])
             ->whereIn("a.employee_id", $employee_ids)
             ->select('a.employee_id', 'b.title', 'b.id')
             ->get();
@@ -764,6 +770,7 @@ class EmployeeService extends Service
         $model = new Role();
         $model = $model->where('id',$data['id'])->first();
         $model->title = $data['title'];
+        $model->tree_type = $user['select_tree_type'];
         $model->save();
 
         return [true,''];
@@ -776,6 +783,7 @@ class EmployeeService extends Service
         $model = new Role();
         $model->title = $data['title'] ;
         $model->top_depart_id = $data['top_depart_id'];
+        $model->tree_type = $user['select_tree_type'];
         $model->save();
 
         return [true,''];
@@ -815,8 +823,10 @@ class EmployeeService extends Service
     }
 
     public function roleList($data,$user){
+        $select_tree_type = $user['select_tree_type'];
         $model = Role::TopClear($user,$data);
         $model = $model->where('del_time',0)
+            ->where('tree_type', $select_tree_type)
             ->select('title','crt_time','id','upd_time')
             ->orderBy('id','desc');
         if(! empty($data['title'])) $model->where('title', 'LIKE', '%' . $data['title'] . '%');

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 727 - 21
app/Service/ItemService.php


+ 1 - 0
app/Service/LoginService.php

@@ -6,6 +6,7 @@ use App\Model\Depart;
 use App\Model\Employee;
 use App\Model\EmployeeDepartPermission;
 use App\Model\EmployeeRole;
+use App\Model\Role;
 use App\Model\RoleMenu;
 use App\Model\RoleMenuButton;
 use App\Model\SysMenu;

+ 2 - 0
app/Service/MiddleGroundService.php

@@ -25,6 +25,7 @@ class MiddleGroundService extends Service
         $model = new Role();
         $model = $model->where('id',$data['id'])->first();
         $model->title = $data['title'];
+        $model->tree_type = 2;
         $model->save();
 
         return [true,''];
@@ -37,6 +38,7 @@ class MiddleGroundService extends Service
         $model = new Role();
         $model->title = $data['title'] ;
         $model->top_depart_id = $user['top_depart_id'];
+        $model->tree_type = 2;
         $model->save();
 
         return [true,''];

+ 124 - 0
app/Service/ProgressCalculatorService.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace App\Service;
+
+
+use App\Model\Item;
+use App\Model\ItemNode;
+use App\Model\ItemNodeMission;
+
+class ProgressCalculatorService extends Service
+{
+    // 定义常量,方便调用
+    const TYPE_MISSION = 'mission';
+    const TYPE_NODE    = 'node';
+    const TYPE_ITEM    = 'item';
+
+    /**
+     * 统一进度计算入口
+     * @param string $type 发生变更的层级类型: mission, node, item
+     * @param int $id 对应层级的主键 ID
+     */
+    public static function calculate($type, $id) {
+        if (empty($id)) return;
+
+        switch ($type) {
+            case self::TYPE_MISSION:
+                // 1. 如果是任务变了,找到未删除的任务,进而拿到它所属的节点 ID
+                $mission = ItemNodeMission::where('id', $id)
+                    ->where('del_time', 0)
+                    ->first();
+                if ($mission) {
+                    self::handleNodeProgress($mission->item_node_id);
+                }
+                break;
+
+            case self::TYPE_NODE:
+                // 2. 如果是节点变了,直接计算该节点
+                self::handleNodeProgress($id);
+                break;
+
+            case self::TYPE_ITEM:
+                // 3. 如果是项目层级触发,直接计算项目
+                self::handleItemProgress($id);
+                break;
+        }
+    }
+
+    /**
+     * 核心逻辑:计算节点进度并自动向上触发项目计算(私有,不对外)
+     */
+    private static function handleNodeProgress($nodeId) {
+        // 严格限制:只有未删除的节点才参与计算
+        $node = ItemNode::where('id', $nodeId)
+            ->where('del_time', 0)
+            ->first();
+        if (!$node) return;
+
+        // 查找该节点下所有未删除的任务
+        $missions = ItemNodeMission::where('item_node_id', $nodeId)
+            ->where('del_time', 0)
+            ->get();
+
+        if ($missions->isEmpty()) {
+            // 无任务,进度由自身状态决定
+            $node->progress = ($node->state == ItemNodeMission::TYPE_THREE) ? 100.00 : 0.00;
+        } else {
+            $totalWeight = $missions->sum('mission_weight');
+            if ($totalWeight <= 0) {
+                // 如果建了任务但权重全是 0,按任务个数平均分
+                $completedCount = $missions->where('state', 3)->count();
+                $node->progress = round(($completedCount / $missions->count()) * 100, 2);
+            } else {
+                // 已完成(state = 3)的任务权重和
+                $completedWeight = $missions->where('state', 3)->sum('mission_weight');
+                $node->progress = round(($completedWeight / $totalWeight) * 100, 2);
+            }
+        }
+        $node->save();
+
+        // 【自动冒泡】节点算完,自动去算它所属的项目
+        self::handleItemProgress($node->item_id);
+    }
+
+    /**
+     * 核心逻辑:计算项目进度(私有,不对外)
+     */
+    private static function handleItemProgress($itemId) {
+        // 严格限制:只有未删除的项目才参与计算
+        $item = Item::where('id', $itemId)
+            ->where('del_time', 0)
+            ->first();
+        if (!$item) return;
+
+        // 查找该项目下所有未删除的节点
+        $nodes = ItemNode::where('item_id', $itemId)
+            ->where('del_time', 0)
+            ->get();
+
+        if ($nodes->isEmpty()) {
+            // 无节点,进度由自身状态决定
+            $item->progress = ($item->state == ItemNode::TYPE_THREE) ? 100.00 : 0.00;;
+            $item->save();
+            return;
+        }
+
+        $totalWeight = $nodes->sum('node_weight');
+        if ($totalWeight <= 0) {
+            // 如果有节点但权重全写了 0,按节点个数平均分配
+            $currentSum = 0;
+            foreach ($nodes as $node) {
+                $currentSum += ($node->progress / 100);
+            }
+            $item->progress = round(($currentSum / $nodes->count()) * 100, 2);
+        } else {
+            // 采用复合算法:SUM(节点权重 * 节点自身的 progress) / 总权重
+            $currentSum = 0;
+            foreach ($nodes as $node) {
+                $currentSum += $node->node_weight * ($node->progress / 100);
+            }
+            $item->progress = round(($currentSum / $totalWeight) * 100, 2);
+        }
+        $item->save();
+    }
+}

+ 1 - 1
app/Service/SysMenuService.php

@@ -196,7 +196,7 @@ class SysMenuService extends Service
                 ->where('menu_id',$data['id'])
                 ->pluck('id')->all();
         }
-        if($bool) return [false,'菜单名称已经存在'];
+//        if($bool) return [false,'菜单名称已经存在'];
 
         //按钮
         if(! empty($data['button'])){

+ 2 - 1
app/Service/TagService.php

@@ -72,7 +72,8 @@ class TagService extends Service
         $model = Tag::TopClear($user,$data);
         $model = $model->where('del_time',0)
             ->select($field)
-            ->orderby('id', 'asc');
+            ->orderby('sort', 'asc')
+            ->orderby('id', 'desc');
 
         if(! empty($data['title'])) $model->where('title', 'LIKE', '%'.$data['title'].'%');
         if(! empty($data['code'])) $model->where('code', 'LIKE', '%'.$data['code'].'%');

+ 592 - 0
app/Service/WorkFlowService.php

@@ -0,0 +1,592 @@
+<?php
+
+namespace App\Service;
+
+use App\Model\Draft;
+use App\Model\Employee;
+use App\Model\WorkFlowInstances;
+use App\Model\WorkFlowInstancesNodes;
+use App\Model\WorkFlowTemplates;
+use Illuminate\Support\Facades\DB;
+
+class WorkFlowService extends Service
+{
+    public function workFlowEdit($data,$user){
+        list($status,$msg) = $this->workFlowRule($data, $user, false);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = WorkFlowTemplates::where('id', $data['id'])->first();
+            $model->code = $data['code'] ?? '';
+            $model->title = $data['title'] ?? '';
+            $model->content = $data['content'] ?? '';
+            $model->save();
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    public function workFlowAdd($data,$user){
+        list($status,$msg) = $this->workFlowRule($data, $user);
+        if(!$status) return [$status,$msg];
+
+        try {
+            DB::beginTransaction();
+
+            $model = new WorkFlowTemplates();
+            $model->code = $data['code'] ?? '';
+            $model->title = $data['title'] ?? '';
+            $model->content = $data['content'] ?? '';
+            $model->crt_id = $user['id'];
+            $model->top_depart_id = $user['top_depart_id'];
+            $model->save();
+
+            DB::commit();
+        }catch (\Exception $exception){
+            DB::rollBack();
+            return [false,$exception->getMessage()];
+        }
+
+        return [true, ''];
+    }
+
+    public function workFlowDetail($data, $user){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+        $customer = WorkFlowTemplates::where('del_time',0)
+            ->where('id',$data['id'])
+            ->first();
+        if(empty($customer)) return [false,'审批流不存在或已被删除'];
+        $customer = $customer->toArray();
+
+        $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']): '';
+
+        return [true, $customer];
+    }
+
+    public function workFlowDel($data, $user){
+        if($this->isEmpty($data,'id')) return [false,'请选择数据!'];
+        $customer = WorkFlowTemplates::where('del_time',0)
+            ->where('id',$data['id'])
+            ->first();
+        if(empty($customer)) return [false,'审批流不存在或已被删除'];
+        $customer->del_time = time();
+        $customer->save();
+
+        return [true, ''];
+    }
+
+    public function workFlowCommon($data,$user, $field = []){
+        if(empty($field)) $field = WorkFlowTemplates::$field;
+
+        $model = WorkFlowTemplates::TopClear($user,$data);
+        $model = $model->where('del_time',0)
+            ->select($field)
+            ->orderby('id', 'desc');
+
+        if(! empty($data['title'])) $model->where('title', 'LIKE', '%'.$data['title'].'%');
+        if(! empty($data['code'])) $model->where('code', 'LIKE', '%'.$data['code'].'%');
+        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 workFlowList($data,$user){
+        $model = $this->workFlowCommon($data, $user);
+        $list = $this->limit($model,'',$data);
+        $list = $this->fillData($list);
+
+        return [true, $list];
+    }
+
+    public function workFlowRule(&$data, $user, $is_add = true) {
+        if(empty($data['title'])) return [false, '审批流名称不能为空'];
+        if(empty($data['code'])) return [false, '审批流编码不能为空'];
+        if(empty($data['content'])) return [false, '审批流内容不能为空'];
+
+        // 1. JSON 格式校验与转换
+        $content = $data['content'];
+        if (is_string($content)) {
+            $content = json_decode($content, true);
+            if (json_last_error() !== JSON_ERROR_NONE) {
+                return [false, '审批流内容格式错误,不是有效的JSON'];
+            }
+        }
+
+        // 2. 结构完整性校验
+        if (!isset($content['nodes']) || !is_array($content['nodes'])) {
+            return [false, '审批流配置缺少节点(nodes)信息'];
+        }
+        if (!isset($content['edges']) || !is_array($content['edges'])) {
+            return [false, '审批流配置缺少连线(edges)信息'];
+        }
+
+        $data['content'] = $content;
+
+        // 4. 业务唯一性校验
+        $query = WorkFlowTemplates::where('code', $data['code'])
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->where('del_time', 0);
+
+        if (!$is_add) {
+            if (empty($data['id'])) return [false, 'ID不能为空'];
+            $query->where('id', '<>', $data['id']);
+        }
+
+        if ($query->exists()) return [false, '审批流编码已存在'];
+
+        return [true, ''];
+    }
+
+    public function fillData($data){
+        if(empty($data['data'])) return $data;
+
+        $emp = (new EmployeeService())->getEmployeeMap(array_unique(array_merge_recursive(array_column($data['data'],'charge_id'), 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]['crt_name'] = $emp[$value['crt_id']] ?? '';
+        }
+
+        return $data;
+    }
+
+    /**
+     * 触发审批流实例
+     * * @param string $templateCode 审批流唯一编码
+     * @param int $documentId 业务单据ID
+     * @param string $documentType 业务模型类名
+     * @param array $user 当前操作用户信息
+     */
+    public function triggerWorkflow($templateID, $documentId, $documentType, $user)
+    {
+        // 1. 获取对应的审批流模板
+        $template = WorkFlowTemplates::where('id', $templateID)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->where('del_time', 0)
+            ->first();
+
+        if (!$template) return [false, '审批模板不存在'];
+
+        // 2. 创建流程主实例
+        $instance = WorkFlowInstances::create([
+            'template_id'   => $template->id,
+            'document_id'   => $documentId,
+            'document_type' => $documentType,
+            'status'        => WorkFlowInstances::status_two, // 激活
+            'crt_id'        => $user['id'],
+            'crt_time'      => time(),
+            'upd_time'      => time(),
+            'top_depart_id' => $user['top_depart_id']
+        ]);
+
+        // 获取前端原始 JSON 中的节点和连线 [cite: 1]
+        $nodes = $template->content['nodes'];
+        $edges = $template->content['edges'];
+
+        // 3. 准备批量插入的节点数据
+        $insertNodes = [];
+        foreach ($nodes as $node) {
+            $nodeKey = $node['id'];
+
+            // 从 edges 中寻找当前节点的前置 (source 为谁) 和 后置 (target 为谁) [cite: 1]
+            // 这里的 source 和 target 是 edges 顶层的字符串 ID
+            $prev = collect($edges)->where('target', $nodeKey)->pluck('source')->first();
+            $next = collect($edges)->where('source', $nodeKey)->pluck('target')->first();
+
+            $tempAssignees = $node['data']['assignees'] ?? [];
+            foreach($tempAssignees as $k => $person) {
+                $tempAssignees[$k]['is_pass'] = 0; // 0-未审,1-通过,2-驳回
+                $tempAssignees[$k]['mark'] = '';
+                $tempAssignees[$k]['pass_time'] = 0;
+            }
+
+            $insertNodes[] = [
+                'instance_id'   => $instance->id,
+                'node_key'      => $nodeKey,
+                'label'         => $node['label'] ?? '未命名节点',
+                'prev_node_key' => $prev, // 上级节点 ID
+                'next_node_key' => $next, // 下级节点 ID
+
+                // 审批人快照存储 [cite: 1]
+                'assignees'     => json_encode($tempAssignees),
+
+                // 审批类型:1 会签,2 或签 [cite: 1]
+                'approval_type' => $node['data']['type'] ?? 2,
+
+                // 初始状态逻辑:如果没有前置节点,说明是首节点,直接进入“审批中”状态
+                'status'        => $prev ? 0 : 1,
+
+                'handled_time'  => 0,
+                'crt_time'      => time(),
+                'upd_time'      => time(),
+                'crt_id'        => $user['id'],
+                'top_depart_id' => $user['top_depart_id']
+            ];
+        }
+
+        // 4. 写入节点实例表
+        WorkFlowInstancesNodes::insert($insertNodes);
+
+        return [true, $instance->id];
+    }
+
+    /**
+     * 获取当前用户的已办(已处理)审核列表
+     */
+    public function getMyHandledApprovals($data, $user)
+    {
+        $userId = $user['id'];
+        $topDepartId = $user['top_depart_id'];
+
+        // 1. 构造查询对象
+        $model = WorkFlowInstancesNodes::where('del_time', 0)
+            ->where('top_depart_id', $topDepartId)
+            // 核心条件变更:
+            // 条件A:我是审批人之一
+            ->whereRaw('JSON_CONTAINS(assignees, JSON_OBJECT("id", ?))', [$userId])
+            // 条件B:包含我已经审批通过(1)或审批驳回(2)的情况
+            ->whereRaw('(JSON_CONTAINS(assignees, JSON_OBJECT("id", ?, "is_pass", 1)) OR JSON_CONTAINS(assignees, JSON_OBJECT("id", ?, "is_pass", 2)))', [$userId, $userId])
+            ->select('*')
+            ->orderBy('id', 'desc');
+
+        $pageData = $this->limit($model, '', $data);
+
+        if (empty($pageData['data'])) return [true, $pageData];
+
+        // 2. 提取当前页的所有流程实例 ID
+        $instanceIds = array_unique(array_column($pageData['data'], 'instance_id'));
+
+        // 3. 获取流程实例及其关联的模板
+        $instances = WorkFlowInstances::with(['template'])
+            ->whereIn('id', $instanceIds)
+            ->get()
+            ->keyBy('id');
+
+        // --- 处理动态业务单据数据 ---
+        // 4. 按业务类型分组,批量查出单据的 code 和 title
+        $documentGroups = [];
+        foreach ($instances as $ins) {
+            $documentGroups[$ins->document_type][] = $ins->document_id;
+        }
+
+        $documentDataMap = []; // 存储结构: [type][id] => ['code' => ..., 'title' => ...]
+        foreach ($documentGroups as $type => $ids) {
+            $docs = DB::table($type)
+                ->whereIn('id', array_unique($ids))
+                ->select('id', 'code', 'title')
+                ->get()
+                ->keyBy('id')
+                ->toArray();
+            $documentDataMap[$type] = $docs;
+        }
+
+        // 5. 组装最终返回数据
+        foreach ($pageData['data'] as $key => $node) {
+            $instance = $instances[$node['instance_id']] ?? null;
+
+            // 获取关联业务表的具体数据
+            $docDetail = null;
+            if ($instance) {
+                $docDetail = $documentDataMap[$instance->document_type][$instance->document_id] ?? null;
+            }
+
+            // 解析出我个人的审批意见和审批时间
+            $assignees = is_string($node['assignees']) ? json_decode($node['assignees'], true) : $node['assignees'];
+            $mySnapshot = collect($assignees)->firstWhere('id', $userId);
+
+            $pageData['data'][$key] = [
+                'node_instance_id'   => $node['id'],
+                'instance_id'        => $node['instance_id'],
+                'node_label'         => $node['label'],
+                'document_id'        => $instance->document_id ?? 0,
+                'document_type'      => $instance->document_type ?? '',
+                'document_code'      => $docDetail->code ?? '',  // 业务单据编号
+                'document_title'     => $docDetail->title ?? '', // 业务单据标题
+                'workflow_title'     => $instance->template->title ?? '', // 审批模板名
+                'crt_time'           => !empty($instance->crt_time->timestamp) ? date('Y-m-d H:i:s', $instance->crt_time->timestamp) : '',
+                'approval_type'      => $node['approval_type'],
+                'approval_type_title'=> WorkFlowInstancesNodes::approval_type[$node['approval_type']] ?? '',
+
+                // --- 已办列表额外返回:我当时的处理结果 ---
+                'my_handle_status'   => $mySnapshot['is_pass'] ?? 0, // 我个人的处理状态:1-通过,2-驳回
+                'my_handle_status_title'   => WorkFlowInstancesNodes::is_pass_title[$mySnapshot['is_pass']] ?? '', // 我个人的处理状态:1-通过,2-驳回
+                'my_handle_remark'   => $mySnapshot['mark'] ?? '',   // 我写下的审批意见
+                'my_handle_time_fmt' => !empty($mySnapshot['pass_time']) ? date('Y-m-d H:i:s', $mySnapshot['pass_time']) : '', // 我的审批时间
+
+                // 整个流程目前的整体状态(方便前端展示:审批中、已通过、已驳回)
+                'instance_status'    => $instance->status ?? 0,
+            ];
+        }
+
+        return [true, $pageData];
+    }
+
+    /**
+     * 获取当前用户的待办审核列表
+     */
+    public function getMyPendingApprovals($data, $user)
+    {
+        $userId = $user['id'];
+        $topDepartId = $user['top_depart_id'];
+
+        // 1. 构造查询对象
+        $model = WorkFlowInstancesNodes::where('del_time', 0)
+            ->where('top_depart_id', $topDepartId)
+            ->where('status', 1)
+            ->whereRaw('JSON_CONTAINS(assignees, JSON_OBJECT("id", ?))', [$userId])
+            ->whereRaw('NOT JSON_CONTAINS(assignees, JSON_OBJECT("id", ?, "is_pass", 1))', [$userId])
+            ->select('*')
+            ->orderBy('id', 'desc');
+
+        $pageData = $this->limit($model, '', $data);
+
+        if (empty($pageData['data'])) return [true, $pageData];
+
+        // 2. 提取当前页的所有流程实例 ID
+        $instanceIds = array_unique(array_column($pageData['data'], 'instance_id'));
+
+        // 3. 获取流程实例及其关联的模板
+        $instances = WorkFlowInstances::with(['template'])
+            ->whereIn('id', $instanceIds)
+            ->get()
+            ->keyBy('id');
+
+        // --- 处理动态业务单据数据 ---
+        // 4. 按业务类型分组,批量查出单据的 code 和 title
+        $documentGroups = [];
+        foreach ($instances as $ins) {
+            $documentGroups[$ins->document_type][] = $ins->document_id;
+        }
+
+        $documentDataMap = []; // 存储结构: [type][id] => ['code' => ..., 'title' => ...]
+        foreach ($documentGroups as $type => $ids) {
+            // 这里根据你的 document_type (如 'item') 动态查询
+            // 假设你的业务表都有 code 和 title 字段
+            $docs = DB::table($type)
+                ->whereIn('id', array_unique($ids))
+                ->select('id', 'code', 'title')
+                ->get()
+                ->keyBy('id')
+                ->toArray();
+            $documentDataMap[$type] = $docs;
+        }
+
+        // 5. 组装最终返回数据
+        foreach ($pageData['data'] as $key => $node) {
+            $instance = $instances[$node['instance_id']] ?? null;
+
+            // 获取关联业务表的具体数据
+            $docDetail = null;
+            if ($instance) {
+                $docDetail = $documentDataMap[$instance->document_type][$instance->document_id] ?? null;
+            }
+
+            $pageData['data'][$key] = [
+                'node_instance_id' => $node['id'],
+                'instance_id'      => $node['instance_id'],
+                'node_label'       => $node['label'],
+                'document_id'      => $instance->document_id ?? 0,
+                'document_type'    => $instance->document_type ?? '',
+                // --- 新增字段 ---
+                'document_code'    => $docDetail->code ?? '',  // 业务单据编号
+                'document_title'   => $docDetail->title ?? '', // 业务单据标题
+                'workflow_title'       => $instance->template->title ?? '', // 审批模板名
+                'crt_time'   => !empty($instance->crt_time->timestamp) ? date('Y-m-d H:i:s', $instance->crt_time->timestamp) : '',
+                'approval_type'    => $node['approval_type'],
+                'approval_type_title'    => WorkFlowInstancesNodes::approval_type[$node['approval_type']] ?? '',
+            ];
+        }
+
+        return [true, $pageData];
+    }
+
+    public function approval($data, $user)
+    {
+        DB::beginTransaction(); // 开启最外层事务
+        try {
+            // 1. 执行审批逻辑
+            list($status, $msg, $result) = $this->approve($data, $user);
+
+            if (!$status) {
+                DB::rollBack();
+                return [false, $msg];
+            }
+
+            // 2. 如果流程终结(通过或驳回),执行业务结算
+            if ($result == 1 || $result == 2) {
+                $instance_id = $msg;
+
+                $this->businessSettle($result, $instance_id);
+            }
+
+            DB::commit();
+
+            return [true, ''];
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "审批失败: " . $e->getMessage()];
+        }
+    }
+
+    /**
+     * @param int $nodeInstanceId 节点实例ID
+     * @param int $status 结果:2-通过,3-驳回
+     * @param string $remark 意见
+     * @param array $user 当前操作人
+     *
+     * 业务结果说明:0-审批中(继续等待), 1-最终通过(业务单据可改为已完成), 2-已驳回(业务单据可改为已拒绝)
+     */
+    public function approve($data, $user)
+    {
+        $nodeInstanceId = $data['node_instance_id'] ?? 0;
+        $status = $data['status'] ?? 2;
+        $remark = $data['mark'] ?? '';
+
+        $currentNode = WorkFlowInstancesNodes::where('del_time',0)
+            ->where('id', $nodeInstanceId)
+            ->where('top_depart_id', $user['top_depart_id'])
+            ->first();
+
+        if (!$currentNode || $currentNode->status != 1) return [false, '环节无效或已处理', 0];
+
+        $assignees = is_string($currentNode->assignees) ? json_decode($currentNode->assignees, true) : $currentNode->assignees;
+
+        $myIndex = collect($assignees)->search(fn($item) => $item['id'] == $user['id']);
+        if ($myIndex === false) return [false, '您不在审批人名单中', 0];
+
+        $time = time();
+        $assignees[$myIndex]['mark'] = $remark;
+        $assignees[$myIndex]['pass_time'] = $time;
+
+        // --- 情况 A:驳回 ---
+        if ($status == 3) {
+            $currentNode->status = 3;
+            $currentNode->handled_time = $time;
+            $assignees[$myIndex]['is_pass'] = 2;
+            $currentNode->assignees = $assignees;
+            $currentNode->save();
+
+            WorkFlowInstances::where('id', $currentNode->instance_id)->update(['status' => WorkFlowInstances::status_four]);
+
+            DB::commit();
+            return [true, $currentNode->instance_id, 2];
+        }
+
+        // --- 情况 B:通过 ---
+        $assignees[$myIndex]['is_pass'] = 1;
+        $currentNode->assignees = $assignees;
+
+        $shouldMoveToNext = false;
+        if ($currentNode->approval_type == 2) {
+            $shouldMoveToNext = true;
+        } else {
+            $unPassedCount = collect($assignees)->where('is_pass', '!=', 1)->count();
+            if ($unPassedCount == 0) {
+                $shouldMoveToNext = true;
+            }
+        }
+
+        $finalResult = 0; // 默认:还在审批中
+
+        if ($shouldMoveToNext) {
+            $currentNode->status = 2;
+            $currentNode->handled_time = $time;
+            $currentNode->save();
+
+            if (!empty($currentNode->next_node_key)) {
+                $hasActiveNext = $this->activateNextNode($currentNode->instance_id, $currentNode->next_node_key);
+                if (!$hasActiveNext) {
+                    // 如果逻辑上虽然有key但没找到下个节点,也视为终结
+                    WorkFlowInstances::where('id', $currentNode->instance_id)->update(['status' => WorkFlowInstances::status_three]);
+                    $finalResult = 1;
+                }
+            } else {
+                // 没有下个节点,流程最终终结
+                WorkFlowInstances::where('id', $currentNode->instance_id)->update(['status' => WorkFlowInstances::status_three]);
+                $finalResult = 1; // 返回 1,表示业务层面可以执行“生效”动作
+            }
+        } else {
+            // 会签中,还有人没审
+            $currentNode->save();
+            $finalResult = 0;
+        }
+
+        return [true, $currentNode->instance_id, $finalResult];
+    }
+
+    private function activateNextNode($instanceId, $nextNodeKey)
+    {
+        $nextNode = WorkFlowInstancesNodes::where('instance_id', $instanceId)
+            ->where('node_key', $nextNodeKey)
+            ->where('del_time',0)
+            ->first();
+
+        if ($nextNode) {
+            $nextNode->status = 1;
+            $nextNode->save();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 业务结算逻辑
+     */
+    public function businessSettle($result, $instance_id)
+    {
+        $w = WorkFlowInstances::where('id', $instance_id)->first();
+        if (empty($w)) throw new \Exception("未找到流程实例");
+
+        $draft = Draft::where('del_time', 0)
+            ->where('document_type', $w->document_type)
+            ->where('document_id', $w->document_id)
+            ->where('top_depart_id', $w->top_depart_id)
+            ->latest()
+            ->first();
+
+        // 审核最终通过
+        if ($result == 1) {
+            if (!empty($draft)) {
+                if ($draft->opt_type == 1) {
+                    // 执行覆盖逻辑
+                    $status = false;
+                    $resMsg = '';
+
+                    // 动态调用
+                    if ($draft->document_type == 'item') {
+                        list($status, $resMsg) = (new ItemService())->itemEditSave($draft->content, $draft->opt_user);
+                    } elseif ($draft->document_type == 'item_node') {
+                        list($status, $resMsg) = (new ItemService())->itemNodeEditSave($draft->content, $draft->opt_user);
+                    } elseif ($draft->document_type == 'item_node_mission') {
+                        list($status, $resMsg) = (new ItemService())->itemNodeMissionEditSave($draft->content, $draft->opt_user);
+                    }
+
+                    if (!$status) throw new \Exception("业务数据更新失败: " . $resMsg);
+                }elseif ($draft->opt_type == 2){
+                    DB::table($w->document_type)->where('id', $w->document_id)->update($draft->content);
+                }
+            }
+        }
+
+        // 无论通过还是驳回,重置业务表状态
+        DB::table($w->document_type)->where('id', $w->document_id)->update(['approval_state' => 0]);
+
+        // 清理草稿
+        if (! empty($draft)) {
+            $draft->del_time = time();
+            $draft->save();
+        }
+    }
+}

+ 23 - 3
routes/api.php

@@ -113,13 +113,19 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('itemNodeFinish', 'Api\ItemController@itemNodeFinish');
 
     //项目节点任务
-    $route->any('itemNodeMissionList', 'Api\ItemController@itemNodeMissionList');
-    $route->any('itemNodeMissionList', 'Api\ItemController@itemNodeMissionList');
+    $route->any('itemNodeMissionListBySearch', 'Api\ItemController@itemNodeMissionListBySearch'); //不分页 带 项目 节点查询
+    $route->any('itemNodeMissionDetailBoard', 'Api\ItemController@itemNodeMissionDetailBoard');
     $route->any('itemNodeMissionEdit', 'Api\ItemController@itemNodeMissionEdit');
     $route->any('itemNodeMissionAdd', 'Api\ItemController@itemNodeMissionAdd');
     $route->any('itemNodeMissionDel', 'Api\ItemController@itemNodeMissionDel');
     $route->any('itemNodeMissionDetail', 'Api\ItemController@itemNodeMissionDetail');
-    $route->any('itemNodeMissionFinish', 'Api\ItemController@itemNodeMissionFinish');
+    $route->any('itemNodeMissionFinish', 'Api\ItemController@itemNodeMissionFinish'); // 完结
+    $route->any('itemNodeMissionUpdateProgress', 'Api\ItemController@itemNodeMissionUpdateProgress'); // 更新任务进展
+    $route->any('itemNodeMissionUpdateProgressContent', 'Api\ItemController@itemNodeMissionUpdateProgressContent'); // 更新任务进展人员日报
+    $route->any('itemNodeMissionUpdateState', 'Api\ItemController@itemNodeMissionUpdateState'); // 拖拽更新任务状态
+    $route->any('itemNodeMissionList', 'Api\ItemController@itemNodeMissionList');
+    $route->any('itemGannetList', 'Api\ItemController@itemGannetList');
+    $route->any('itemGannetGetSonData', 'Api\ItemController@itemGannetGetSonData');
 
     //标签管理
     $route->any('tagList', 'Api\TagController@tagList');
@@ -135,6 +141,20 @@ Route::group(['middleware'=> ['checkLogin']],function ($route){
     $route->any('teamDel', 'Api\TeamController@teamDel');
     $route->any('teamDetail', 'Api\TeamController@teamDetail');
 
+    //审批流模板
+    $route->any('workFlowList', 'Api\WorkFlowController@workFlowList');
+    $route->any('workFlowEdit', 'Api\WorkFlowController@workFlowEdit');
+    $route->any('workFlowAdd', 'Api\WorkFlowController@workFlowAdd');
+    $route->any('workFlowDel', 'Api\WorkFlowController@workFlowDel');
+    $route->any('workFlowDetail', 'Api\WorkFlowController@workFlowDetail');
+
+    //待我审核
+    $route->any('getMyPendingApprovals', 'Api\WorkFlowController@getMyPendingApprovals');
+    //已完成
+    $route->any('getMyHandledApprovals', 'Api\WorkFlowController@getMyHandledApprovals');
+    //审核接口
+    $route->any('approval', 'Api\WorkFlowController@approval');
+
     //费用类型
     $route->any('feeAdd', 'Api\FeeController@feeAdd')->name('fee.add');
     $route->any('feeEdit', 'Api\FeeController@feeEdit')->name('fee.edit');

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů