PM_Console/backend/app/Http/Controllers/Api/SprintController.php

426 خطوط
17 KiB
PHP
خام سرزنش تاریخچه

مخزن ambiguous runes header

راهنمای مخزن ambiguous runes توضیحات

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\SprintResource;
use App\Http\Resources\TaskResource;
use App\Models\Sprint;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class SprintController extends Controller
{
private array $statuses = ['planning', 'active', 'completed', 'cancelled'];
public function index(Request $request): JsonResponse
{
try {
$query = Sprint::with(['project', 'tasks', 'members']);
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->boolean('mine')) {
$query->whereHas('members', fn($q) => $q->where('users.id', $request->user()->id));
}
$perPage = min((int) $request->input('per_page', 15), 100);
$sortBy = in_array($request->input('sort_by'), ['id', 'title', 'status', 'created_at', 'updated_at', 'start_date', 'end_date'], true)
? $request->input('sort_by')
: 'created_at';
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
$sprints = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => SprintResource::collection($sprints),
'meta' => [
'current_page' => $sprints->currentPage(),
'last_page' => $sprints->lastPage(),
'per_page' => $sprints->perPage(),
'total' => $sprints->total(),
],
'message' => 'لیست Sprintها',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت Sprintها'], 500);
}
}
public function show(Sprint $sprint): JsonResponse
{
try {
$sprint->load([
'project',
'tasks.project',
'tasks.assignee',
'tasks.reporter',
'members',
'creator',
'retrospective',
]);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint),
'message' => 'اطلاعات Sprint',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت اطلاعات Sprint'], 500);
}
}
public function store(Request $request): JsonResponse
{
try {
$data = $this->validatedSprintData($request);
$memberIds = $data['member_ids'] ?? [];
unset($data['member_ids']);
$data['created_by'] = $request->user()->id;
$data['status'] = 'planning';
$sprint = DB::transaction(function () use ($data, $memberIds) {
$sprint = Sprint::create($data);
$sprint->members()->sync($memberIds);
return $sprint;
});
$sprint->load(['project', 'tasks', 'members']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint),
'message' => 'Sprint با موفقیت ایجاد شد',
], 201);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در ایجاد Sprint'], 500);
}
}
public function update(Request $request, Sprint $sprint): JsonResponse
{
try {
$data = $this->validatedSprintData($request, true);
$memberIds = $data['member_ids'] ?? null;
unset($data['member_ids']);
DB::transaction(function () use ($sprint, $data, $memberIds) {
$sprint->update($data);
if (is_array($memberIds)) {
$sprint->members()->sync($memberIds);
}
});
$sprint->load(['project', 'tasks', 'members', 'retrospective']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members', 'retrospective'])),
'message' => 'Sprint به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در به‌روزرسانی Sprint'], 500);
}
}
public function destroy(Sprint $sprint): JsonResponse
{
try {
$sprint->delete();
return response()->json(['success' => true, 'message' => 'Sprint حذف شد']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در حذف Sprint'], 500);
}
}
public function availableTasks(Sprint $sprint): JsonResponse
{
try {
$taskIdsInSprint = $sprint->tasks()->pluck('tasks.id');
$tasks = Task::with(['project', 'assignee'])
->withCount(['comments', 'files'])
->where('project_id', $sprint->project_id)
->whereNotIn('id', $taskIdsInSprint)
->whereNotIn('status', ['done', 'canceled'])
->orderBy('created_at', 'desc')
->get();
return response()->json([
'success' => true,
'data' => TaskResource::collection($tasks),
'message' => 'تسک‌های قابل افزودن به Sprint',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت تسک‌های قابل افزودن'], 500);
}
}
public function addTask(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate(['task_id' => 'required|exists:tasks,id']);
$task = Task::findOrFail($request->task_id);
if ($task->project_id !== $sprint->project_id) {
return response()->json(['success' => false, 'message' => 'این تسک متعلق به پروژه Sprint نیست'], 422);
}
$sprint->tasks()->syncWithoutDetaching([$task->id]);
return response()->json(['success' => true, 'message' => 'تسک به Sprint اضافه شد']);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در اضافه کردن تسک به Sprint'], 500);
}
}
public function removeTask(Sprint $sprint, Task $task): JsonResponse
{
try {
$sprint->tasks()->detach($task->id);
return response()->json(['success' => true, 'message' => 'تسک از Sprint حذف شد']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در حذف تسک از Sprint'], 500);
}
}
public function updateTaskStatus(Request $request, Sprint $sprint, Task $task): JsonResponse
{
try {
$request->validate([
'status' => 'required|string|in:waiting,todo,in_progress,review,done',
]);
if (!$sprint->tasks()->where('tasks.id', $task->id)->exists()) {
return response()->json(['success' => false, 'message' => 'این تسک در Sprint انتخاب‌شده نیست'], 404);
}
$task->update(['status' => $request->status]);
return response()->json([
'success' => true,
'data' => new TaskResource($task->fresh()->load(['project', 'assignee'])),
'message' => 'وضعیت تسک به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در تغییر وضعیت تسک'], 500);
}
}
public function updateStatus(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate(['status' => 'required|string|in:' . implode(',', $this->statuses)]);
$sprint->update(['status' => $request->status]);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members'])),
'message' => 'وضعیت Sprint به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در به‌روزرسانی وضعیت Sprint'], 500);
}
}
public function start(Sprint $sprint): JsonResponse
{
try {
if ($sprint->status !== 'planning') {
return response()->json(['success' => false, 'message' => 'فقط Sprint برنامه‌ریزی شده قابل شروع است'], 422);
}
$activeSprint = Sprint::where('project_id', $sprint->project_id)
->where('status', 'active')
->where('id', '!=', $sprint->id)
->first();
if ($activeSprint) {
return response()->json([
'success' => false,
'message' => "در این پروژه Sprint فعال دیگری وجود دارد: {$activeSprint->title}",
], 422);
}
$sprint->update(['status' => 'active']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members'])),
'message' => 'Sprint شروع شد',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در شروع Sprint'], 500);
}
}
public function end(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate([
'incomplete_action' => 'required|string|in:move_to_next,backlog,keep',
'target_sprint_id' => 'nullable|required_if:incomplete_action,move_to_next|exists:sprints,id',
]);
if ($sprint->status !== 'active') {
return response()->json(['success' => false, 'message' => 'فقط Sprint فعال قابل پایان دادن است'], 422);
}
$incompleteTasks = $sprint->tasks()->where('status', '!=', 'done')->get();
$completedTasks = $sprint->tasks()->where('status', 'done')->count();
$totalTasks = $sprint->tasks()->count();
DB::transaction(function () use ($request, $sprint, $incompleteTasks, $totalTasks, $completedTasks) {
if ($request->incomplete_action === 'move_to_next') {
$targetSprint = Sprint::where('id', $request->target_sprint_id)
->where('project_id', $sprint->project_id)
->where('status', 'planning')
->firstOrFail();
$targetSprint->tasks()->syncWithoutDetaching($incompleteTasks->pluck('id')->all());
$sprint->tasks()->detach($incompleteTasks->pluck('id')->all());
}
if (in_array($request->incomplete_action, ['backlog', 'keep'], true)) {
$sprint->tasks()->detach($incompleteTasks->pluck('id')->all());
}
$sprint->update([
'status' => 'completed',
'completed_at' => now(),
'completion_summary' => [
'total_tasks' => $totalTasks,
'completed_tasks' => $completedTasks,
'incomplete_tasks' => $incompleteTasks->count(),
'incomplete_action' => $request->incomplete_action,
],
]);
});
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members', 'retrospective'])),
'message' => 'Sprint پایان یافت',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در پایان Sprint'], 500);
}
}
public function cancel(Sprint $sprint): JsonResponse
{
try {
if (!in_array($sprint->status, ['planning', 'active'], true)) {
return response()->json(['success' => false, 'message' => 'این Sprint قابل لغو نیست'], 422);
}
$sprint->update(['status' => 'cancelled']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members'])),
'message' => 'Sprint لغو شد',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در لغو Sprint'], 500);
}
}
public function report(Sprint $sprint): JsonResponse
{
try {
$sprint->load(['tasks.assignee', 'members']);
$tasks = $sprint->tasks;
$totalTasks = $tasks->count();
$completedTasks = $tasks->where('status', 'done')->count();
$incompleteTasks = $totalTasks - $completedTasks;
return response()->json([
'success' => true,
'data' => [
'total_tasks' => $totalTasks,
'completed_tasks' => $completedTasks,
'incomplete_tasks' => $incompleteTasks,
'completion_percentage' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0,
'completed_by_member' => $tasks
->where('status', 'done')
->groupBy(fn($task) => $task->assignee?->name ?? 'بدون مسئول')
->map(fn($items, $name) => ['name' => $name, 'completed' => $items->count()])
->values(),
'unfinished_tasks' => TaskResource::collection($tasks->where('status', '!=', 'done')->values()),
],
'message' => 'گزارش Sprint',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت گزارش Sprint'], 500);
}
}
public function updateRetrospective(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate([
'went_well' => 'nullable|string',
'problems' => 'nullable|string',
'improvements' => 'nullable|string',
]);
$sprint->retrospective()->updateOrCreate(
['sprint_id' => $sprint->id],
[
'went_well' => $request->went_well,
'problems' => $request->problems,
'improvements' => $request->improvements,
'updated_by' => $request->user()->id,
]
);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members', 'retrospective'])),
'message' => 'یادداشت پایان Sprint ذخیره شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در ذخیره یادداشت پایان Sprint'], 500);
}
}
private function validatedSprintData(Request $request, bool $partial = false): array
{
$required = $partial ? 'sometimes|required' : 'required';
return $request->validate([
'title' => "{$required}|string|max:255",
'project_id' => "{$required}|exists:projects,id",
'goal' => 'nullable|string|max:1000',
'capacity_hours' => 'nullable|numeric|min:0',
'start_date' => "{$required}|date",
'end_date' => "{$required}|date|after_or_equal:start_date",
'member_ids' => 'nullable|array',
'member_ids.*' => 'exists:users,id',
]);
}
}