resourceAccess->meetings($request->user()) ->with(['project', 'sprint', 'type', 'creator', 'participants']); if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } if ($request->filled('date_from')) { $query->whereDate('date', '>=', $request->date_from); } if ($request->filled('date_to')) { $query->whereDate('date', '<=', $request->date_to); } if ($request->filled('meeting_type')) { $query->where('meeting_type', $request->meeting_type); } if ($request->filled('sprint_id')) { $query->where('sprint_id', $request->sprint_id); } if ($request->filled('status')) { $query->where('status', $request->status); } $perPage = $request->input('per_page', 15); $sortBy = $request->input('sort_by', 'date'); $sortDir = $request->input('sort_dir', 'desc'); $meetings = $query->orderBy($sortBy, $sortDir)->paginate($perPage); return response()->json([ 'success' => true, 'data' => MeetingResource::collection($meetings), 'meta' => [ 'current_page' => $meetings->currentPage(), 'last_page' => $meetings->lastPage(), 'per_page' => $meetings->perPage(), 'total' => $meetings->total(), ], 'message' => 'لیست جلسات', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در دریافت جلسات', ], 500); } } public function show(Meeting $meeting): JsonResponse { try { $meeting->load([ 'project', 'sprint', 'type', 'creator', 'owner', 'facilitator', 'participants', 'actionItems.assignee', 'structuredDecisions', 'structuredActionItems', 'blockers', 'files', ]); return response()->json([ 'success' => true, 'data' => new MeetingResource($meeting), 'message' => 'اطلاعات جلسه', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در دریافت اطلاعات جلسه', ], 500); } } public function store(StoreMeetingRequest $request): JsonResponse { try { $data = $request->validated(); if (! empty($data['project_id'])) { abort_unless( $this->resourceAccess->projects($request->user())->whereKey($data['project_id'])->exists(), 403, 'شما به پروژه انتخاب‌شده دسترسی ندارید.' ); } $data['created_by'] = $request->user()->id; $data['owner_id'] ??= $request->user()->id; $meeting = Meeting::create($data); $meeting->load(['project', 'sprint', 'type', 'creator', 'owner']); return response()->json([ 'success' => true, 'data' => new MeetingResource($meeting), 'message' => 'جلسه با موفقیت ایجاد شد', ], 201); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در ایجاد جلسه', ], 500); } } public function update(Request $request, Meeting $meeting): JsonResponse { try { $data = $request->validate([ 'title' => 'sometimes|required|string|max:255', 'date' => 'sometimes|required|date', 'start_time' => 'nullable', 'end_time' => 'nullable', 'meeting_type' => 'sometimes|required|string|max:50', 'meeting_type_id' => 'nullable|exists:meeting_types,id', 'project_id' => 'nullable|exists:projects,id', 'sprint_id' => 'nullable|exists:sprints,id', 'status' => 'sometimes|string|in:draft,scheduled,in_progress,completed,cancelled,postponed', 'location' => 'nullable|string', 'meeting_link' => 'nullable|url', 'objective' => 'nullable|string', 'agenda' => 'nullable|string', 'notes' => 'nullable|string', 'summary' => 'nullable|string', 'decisions' => 'nullable|string', 'owner_id' => 'nullable|exists:users,id', 'facilitator_id' => 'nullable|exists:users,id', 'reminder_minutes' => 'nullable|integer|min:0|max:10080', 'recurrence_rule' => 'nullable|array', ]); if (isset($data['project_id'])) { abort_unless( $this->resourceAccess->projects($request->user())->whereKey($data['project_id'])->exists(), 403, 'شما به پروژه انتخاب‌شده دسترسی ندارید.' ); } $meeting->update(collect($data)->only([ 'title', 'project_id', 'sprint_id', 'date', 'start_time', 'end_time', 'meeting_type', 'meeting_type_id', 'status', 'location', 'meeting_link', 'objective', 'agenda', 'notes', 'summary', 'decisions', 'owner_id', 'facilitator_id', 'reminder_minutes', 'recurrence_rule', ])->all()); $meeting->load(['project', 'sprint', 'type', 'creator', 'owner', 'facilitator']); return response()->json([ 'success' => true, 'data' => new MeetingResource($meeting->fresh()), 'message' => 'جلسه به‌روزرسانی شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در به‌روزرسانی جلسه', ], 500); } } public function destroy(Meeting $meeting): JsonResponse { try { $meeting->delete(); return response()->json([ 'success' => true, 'message' => 'جلسه حذف شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در حذف جلسه', ], 500); } } public function addParticipant(Request $request, Meeting $meeting): JsonResponse { try { $request->validate(['user_id' => 'required|exists:users,id']); $meeting->participants()->syncWithoutDetaching([$request->user_id]); $this->notificationService->create( $request->user_id, 'meeting_assigned', [ 'meeting_id' => $meeting->id, 'project_id' => $meeting->project_id, 'url' => '/meetings', 'notifiable_type' => 'App\\Models\\Meeting', 'notifiable_id' => $meeting->id, ], "شما به جلسه {$meeting->title} اضافه شدید" ); return response()->json([ 'success' => true, 'message' => 'شرکت‌کننده اضافه شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در اضافه کردن شرکت‌کننده', ], 500); } } public function removeParticipant(Meeting $meeting, User $user): JsonResponse { try { $meeting->participants()->detach($user->id); return response()->json([ 'success' => true, 'message' => 'شرکت‌کننده حذف شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در حذف شرکت‌کننده', ], 500); } } public function addActionItem(Request $request, Meeting $meeting): JsonResponse { try { $request->validate([ 'title' => 'required|string|max:255', 'assigned_to' => 'nullable|exists:users,id', 'due_date' => 'nullable|date', ]); $actionItem = $meeting->actionItems()->create([ 'title' => $request->title, 'assigned_to' => $request->assigned_to, 'due_date' => $request->due_date, 'is_completed' => false, ]); $actionItem->load('assignee'); if ($actionItem->assigned_to) { $this->notificationService->create( $actionItem->assigned_to, 'meeting_action_assigned', [ 'meeting_id' => $meeting->id, 'action_item_id' => $actionItem->id, 'url' => '/meetings', 'notifiable_type' => 'App\\Models\\MeetingActionItem', 'notifiable_id' => $actionItem->id, ], "اقدام جلسه {$actionItem->title} به شما اختصاص داده شد" ); } return response()->json([ 'success' => true, 'data' => new MeetingActionItemResource($actionItem), 'message' => 'ایتم اقدام ایجاد شد', ], 201); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در ایجاد ایتم اقدام', ], 500); } } public function updateActionItem(Request $request, Meeting $meeting, MeetingActionItem $actionItem): JsonResponse { try { $request->validate([ 'title' => 'sometimes|required|string|max:255', 'assigned_to' => 'nullable|exists:users,id', 'due_date' => 'nullable|date', 'is_completed' => 'nullable|boolean', ]); $actionItem->update($request->only(['title', 'assigned_to', 'due_date', 'is_completed'])); $actionItem->load('assignee'); return response()->json([ 'success' => true, 'data' => new MeetingActionItemResource($actionItem->fresh()), 'message' => 'ایتم اقدام به‌روزرسانی شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در به‌روزرسانی ایتم اقدام', ], 500); } } public function convertActionItemToTask(Request $request, Meeting $meeting, MeetingActionItem $actionItem): JsonResponse { try { $request->validate([ 'project_id' => 'required|exists:projects,id', ]); abort_unless( $this->resourceAccess->projects($request->user())->whereKey($request->project_id)->exists(), 403, 'شما به پروژه انتخاب‌شده دسترسی ندارید.' ); $task = Task::create([ 'title' => $actionItem->title, 'project_id' => $request->project_id, 'assignee_id' => $actionItem->assigned_to, 'reporter_id' => $request->user()->id, 'created_by' => $request->user()->id, 'due_date' => $actionItem->due_date, 'status' => 'todo', ]); $actionItem->update(['converted_to_task_id' => $task->id]); $task->load(['project', 'assignee']); return response()->json([ 'success' => true, 'data' => new TaskResource($task), 'message' => 'ایتم اقدام به وظیفه تبدیل شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در تبدیل ایتم اقدام به وظیفه', ], 500); } } public function workspace(Meeting $meeting): JsonResponse { $meeting->load([ 'project', 'sprint', 'type', 'creator', 'owner', 'facilitator', 'participants', 'actionItems.assignee', 'structuredDecisions', 'structuredActionItems', 'blockers', 'files', ]); return response()->json([ 'success' => true, 'data' => new MeetingResource($meeting), 'message' => 'فضای کاری جلسه', ]); } public function start(Request $request, Meeting $meeting): JsonResponse { if (! in_array($meeting->status, ['draft', 'scheduled', 'postponed'], true)) { return response()->json(['success' => false, 'message' => 'این جلسه قابل شروع نیست'], 422); } $meeting->update(['status' => 'in_progress', 'started_at' => now()]); return response()->json([ 'success' => true, 'data' => new MeetingResource($meeting->fresh()), 'message' => 'جلسه شروع شد', ]); } public function complete(Request $request, Meeting $meeting): JsonResponse { $data = $request->validate([ 'summary' => 'nullable|string', 'force' => 'nullable|boolean', ]); if ($meeting->status !== 'in_progress') { return response()->json(['success' => false, 'message' => 'فقط جلسه در حال برگزاری قابل تکمیل است'], 422); } if (blank($data['summary'] ?? $meeting->summary) && ! $request->boolean('force')) { return response()->json(['success' => false, 'message' => 'برای تکمیل جلسه، خلاصه را ثبت کنید یا تأیید ویژه بدهید.'], 422); } $meeting->update([ 'status' => 'completed', 'completed_at' => now(), 'summary' => $data['summary'] ?? $meeting->summary, ]); return response()->json([ 'success' => true, 'data' => new MeetingResource($meeting->fresh()), 'message' => 'جلسه تکمیل شد', ]); } public function effectiveness(Request $request, Meeting $meeting): JsonResponse { $enabled = (bool) (Setting::where('key', 'meeting_effectiveness_enabled')->value('value') ?? false); $reviews = $meeting->effectivenessReviews()->with('user:id,name')->latest()->get(); $ratingFields = ['objective_clarity', 'time_management', 'participation', 'outcome_quality', 'overall_rating']; $averages = collect($ratingFields)->mapWithKeys(fn (string $field) => [ $field => $reviews->isEmpty() ? null : round((float) $reviews->avg($field), 1), ]); return response()->json([ 'success' => true, 'data' => [ 'enabled' => $enabled, 'can_review' => $enabled && $meeting->status === 'completed', 'current_user_review' => $reviews->firstWhere('user_id', $request->user()->id), 'summary' => ['count' => $reviews->count(), 'averages' => $averages], ], 'message' => 'ارزیابی اثربخشی جلسه', ]); } public function storeEffectiveness(Request $request, Meeting $meeting): JsonResponse { $enabled = (bool) (Setting::where('key', 'meeting_effectiveness_enabled')->value('value') ?? false); if (! $enabled) { return response()->json(['success' => false, 'message' => 'ارزیابی اثربخشی جلسه غیرفعال است.'], 422); } if ($meeting->status !== 'completed') { return response()->json(['success' => false, 'message' => 'ارزیابی فقط پس از پایان جلسه قابل ثبت است.'], 422); } $data = $request->validate([ 'objective_clarity' => 'required|integer|between:1,5', 'time_management' => 'required|integer|between:1,5', 'participation' => 'required|integer|between:1,5', 'outcome_quality' => 'required|integer|between:1,5', 'overall_rating' => 'required|integer|between:1,5', 'comment' => 'nullable|string|max:2000', ]); $review = $meeting->effectivenessReviews()->updateOrCreate( ['user_id' => $request->user()->id], $data, ); return response()->json([ 'success' => true, 'data' => $review->fresh()->load('user:id,name'), 'message' => $review->wasRecentlyCreated ? 'ارزیابی جلسه ثبت شد' : 'ارزیابی جلسه به‌روزرسانی شد', ], $review->wasRecentlyCreated ? 201 : 200); } public function addDecision(Request $request, Meeting $meeting): JsonResponse { $data = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'status' => 'nullable|in:proposed,pending,approved,rejected,reversed,superseded', 'owner_id' => 'nullable|exists:users,id', 'rationale' => 'nullable|string', 'impact' => 'nullable|string', ]); $decision = Decision::create($data + [ 'meeting_id' => $meeting->id, 'sprint_id' => $meeting->sprint_id, 'project_id' => $meeting->project_id, 'created_by' => $request->user()->id, ]); return response()->json(['success' => true, 'data' => $decision, 'message' => 'تصمیم ثبت شد'], 201); } public function addStructuredActionItem(Request $request, Meeting $meeting): JsonResponse { $data = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'owner_id' => 'required|exists:users,id', 'due_date' => 'required|date', 'priority' => 'nullable|in:low,medium,high,urgent', ]); $item = ActionItem::create($data + [ 'meeting_id' => $meeting->id, 'sprint_id' => $meeting->sprint_id, 'project_id' => $meeting->project_id, 'created_by' => $request->user()->id, ]); return response()->json(['success' => true, 'data' => $item, 'message' => 'اقدام ثبت شد'], 201); } public function addBlocker(Request $request, Meeting $meeting): JsonResponse { $data = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'severity' => 'nullable|in:low,medium,high,critical', 'owner_id' => 'nullable|exists:users,id', 'task_id' => 'nullable|exists:tasks,id', 'due_date' => 'nullable|date', ]); $blocker = Blocker::create($data + [ 'meeting_id' => $meeting->id, 'sprint_id' => $meeting->sprint_id, 'project_id' => $meeting->project_id, 'raised_by' => $request->user()->id, ]); return response()->json(['success' => true, 'data' => $blocker, 'message' => 'مانع ثبت شد'], 201); } public function types(): JsonResponse { return response()->json([ 'success' => true, 'data' => MeetingType::where('is_active', true)->orderBy('name')->get(), 'message' => 'انواع جلسه', ]); } }