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

538 خطوط
21 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreMeetingRequest;
use App\Http\Resources\MeetingActionItemResource;
use App\Http\Resources\MeetingResource;
use App\Http\Resources\TaskResource;
use App\Models\ActionItem;
use App\Models\Blocker;
use App\Models\Decision;
use App\Models\Meeting;
use App\Models\MeetingActionItem;
use App\Models\MeetingType;
use App\Models\Setting;
use App\Models\Task;
use App\Models\User;
use App\Services\NotificationService;
use App\Services\ResourceAccessService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class MeetingController extends Controller
{
public function __construct(
protected NotificationService $notificationService,
protected ResourceAccessService $resourceAccess,
) {}
public function index(Request $request): JsonResponse
{
try {
$query = $this->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' => 'انواع جلسه',
]);
}
}