538 خطوط
21 KiB
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' => 'انواع جلسه',
|
|
]);
|
|
}
|
|
}
|