diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index da2df8445..eddf07d49 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -1433,7 +1433,7 @@ class ProjectController extends AbstractController // - 非 end|,但 complete_at 有值:视为已完成(兼容无流程或历史数据) if (str_starts_with($task->flow_item_name, 'end')) { $statusText = '已完成'; - if (preg_match('/已取消|Cancelled|취소됨|キャンセル済み|Abgebrochen|Annulé|Dibatalkan|Отменено/', $task->flow_item_name)) { + if (ProjectTask::isCanceledFlowName($task->flow_item_name)) { $statusText = '已取消'; $actualTime = 0; $testTime = 0; diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index 5878143a4..437bd0d08 100755 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -327,6 +327,13 @@ class ReportController extends AbstractController $start_time->startOfWeek(); $end_time = Carbon::instance($start_time)->endOfWeek(); } + // 周报时预计算下一周期时间范围(下周) + $next_start_time = null; + $next_end_time = null; + if ($type === Report::WEEKLY) { + $next_start_time = Carbon::instance($start_time)->copy()->addWeek(); + $next_end_time = Carbon::instance($end_time)->copy()->addWeek(); + } // 生成唯一标识 $sign = Report::generateSign($type, 0, Carbon::instance($start_time)); @@ -350,7 +357,7 @@ class ReportController extends AbstractController Doo::translate('备注'), ]; - // 已完成的任务(排除取消态) + // 已完成的任务 $completeDatas = []; $complete_task = ProjectTask::query() ->whereNotNull("complete_at") @@ -362,8 +369,8 @@ class ReportController extends AbstractController ->get(); if ($complete_task->isNotEmpty()) { foreach ($complete_task as $task) { - // 排除取消态任务:工作流状态名称中包含“取消”相关关键字的视为已取消 - if ($task->flow_item_name && preg_match('/已取消|Cancelled|취소됨|キャンセル済み|Abgebrochen|Annulé|Dibatalkan|Отменено/', $task->flow_item_name)) { + // 排除取消态任务:不将已取消任务计入“已完成工作” + if (ProjectTask::isCanceledFlowName($task->flow_item_name)) { continue; } $complete_at = Carbon::parse($task->complete_at); @@ -381,40 +388,7 @@ class ReportController extends AbstractController // 未完成的任务 $unfinishedDatas = []; - $unfinished_task = ProjectTask::query() - ->join("projects", "projects.id", "=", "project_tasks.project_id") - ->whereNull("projects.archived_at") - ->whereNull("project_tasks.complete_at") - ->whereHas("taskUser", function ($query) use ($user) { - $query->where("userid", $user->userid); - }) - ->where(function ($query) use ($start_time, $end_time) { - // 1) 有计划时间:计划时间与当前周期 [start_time, end_time] 有交集 - $query->where(function ($q1) use ($start_time, $end_time) { - $q1->whereNotNull('project_tasks.start_at') - ->whereNotNull('project_tasks.end_at') - ->where(function ($q2) use ($start_time, $end_time) { - $q2->whereBetween('project_tasks.start_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]) - ->orWhereBetween('project_tasks.end_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]) - ->orWhere(function ($q3) use ($start_time, $end_time) { - $q3->where('project_tasks.start_at', '<=', $start_time->toDateTimeString()) - ->where('project_tasks.end_at', '>=', $end_time->toDateTimeString()); - }); - }); - }) - // 2) 无计划时间:本周期内新建或有更新 - ->orWhere(function ($q1) use ($start_time, $end_time) { - $q1->whereNull('project_tasks.start_at') - ->whereNull('project_tasks.end_at') - ->where(function ($q2) use ($start_time, $end_time) { - $q2->whereBetween('project_tasks.created_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]) - ->orWhereBetween('project_tasks.updated_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]); - }); - }); - }) - ->select("project_tasks.*") - ->orderByDesc("project_tasks.id") - ->get(); + $unfinished_task = ProjectTask::buildUnfinishedTaskQuery($user->userid, $start_time, $end_time, true)->get(); if ($unfinished_task->isNotEmpty()) { foreach ($unfinished_task as $task) { empty($task->end_at) || $end_at = Carbon::parse($task->end_at); @@ -457,15 +431,37 @@ class ReportController extends AbstractController ])->render(); if ($type === Report::WEEKLY) { + // 下周拟定计划:基于下周时间范围预生成候选任务 + $nextPlanDatas = []; + if ($next_start_time && $next_end_time) { + $next_tasks = ProjectTask::buildUnfinishedTaskQuery($user->userid, $next_start_time, $next_end_time, false)->get(); + if ($next_tasks->isNotEmpty()) { + foreach ($next_tasks as $task) { + $planTime = '-'; + if ($task->start_at || $task->end_at) { + $startText = $task->start_at ? Carbon::parse($task->start_at)->format('Y-m-d H:i') : ''; + $endText = $task->end_at ? Carbon::parse($task->end_at)->format('Y-m-d H:i') : ''; + $planTime = trim($startText . ($endText ? (' ~ ' . $endText) : '')); + } + $nextPlanDatas[] = [ + '[' . $task->project->name . '] ' . $task->name, + $planTime, + $task->taskUser->where("owner", 1)->map(function ($item) { + return User::userid2nickname($item->userid); + })->implode(", "), + ]; + } + } + } $contents[] = '

 

'; - $contents[] = "

" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]

"; + $contents[] = "

" . Doo::translate("下周拟定计划") . "[" . $next_start_time->format("m/d") . "-" . $next_end_time->format("m/d") . "]

"; $contents[] = view('report', [ 'labels' => [ Doo::translate('计划描述'), Doo::translate('计划时间'), Doo::translate('负责人'), ], - 'datas' => [], + 'datas' => $nextPlanDatas, ])->render(); } diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index b6a08e2df..38fcf63fc 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -2010,4 +2010,64 @@ class ProjectTask extends AbstractModel // return $task; } + + /** + * 构建指定周期内的未完成任务查询(用于周报/日报等) + * @param int $userid + * @param Carbon $start_time + * @param Carbon $end_time + * @param bool $includeUpdatedForNoPlan 无计划时间任务是否按周期内更新时间一并纳入 + * @return \Illuminate\Database\Eloquent\Builder + */ + public static function buildUnfinishedTaskQuery(int $userid, Carbon $start_time, Carbon $end_time, bool $includeUpdatedForNoPlan = true) + { + return self::query() + ->join("projects", "projects.id", "=", "project_tasks.project_id") + ->whereNull("projects.archived_at") + ->whereNull("project_tasks.complete_at") + ->whereHas("taskUser", function ($query) use ($userid) { + $query->where("userid", $userid); + }) + ->where(function ($query) use ($start_time, $end_time, $includeUpdatedForNoPlan) { + // 1) 有计划时间:计划时间与给定周期 [start_time, end_time] 有交集 + $query->where(function ($q1) use ($start_time, $end_time) { + $q1->whereNotNull('project_tasks.start_at') + ->whereNotNull('project_tasks.end_at') + ->where(function ($q2) use ($start_time, $end_time) { + $q2->whereBetween('project_tasks.start_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]) + ->orWhereBetween('project_tasks.end_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]) + ->orWhere(function ($q3) use ($start_time, $end_time) { + $q3->where('project_tasks.start_at', '<=', $start_time->toDateTimeString()) + ->where('project_tasks.end_at', '>=', $end_time->toDateTimeString()); + }); + }); + }); + // 2) 无计划时间 + $query->orWhere(function ($q1) use ($start_time, $end_time, $includeUpdatedForNoPlan) { + $q1->whereNull('project_tasks.start_at') + ->whereNull('project_tasks.end_at') + ->where(function ($q2) use ($start_time, $end_time, $includeUpdatedForNoPlan) { + $q2->whereBetween('project_tasks.created_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]); + if ($includeUpdatedForNoPlan) { + $q2->orWhereBetween('project_tasks.updated_at', [$start_time->toDateTimeString(), $end_time->toDateTimeString()]); + } + }); + }); + }) + ->select("project_tasks.*") + ->orderByDesc("project_tasks.id"); + } + + /** + * 判断工作流名称是否为取消态(多语言) + * @param string|null $flowItemName + * @return bool + */ + public static function isCanceledFlowName(?string $flowItemName): bool + { + if (empty($flowItemName)) { + return false; + } + return preg_match('/已取消|Cancelled|취소됨|キャンセル済み|Abgebrochen|Annulé|Dibatalkan|Отменено/', $flowItemName) === 1; + } }