>} */ public function readiness(CourseVersion $version): array { $version->load(['modules.lessons.blocks', 'lessons', 'blocks', 'course', 'assessments.questions']); $checks = []; $this->check($checks, 'metadata', trim($version->title) !== '' && trim((string) $version->description) !== '', 'مشخصات دوره', 'عنوان و توضیح دوره کامل است.', 'عنوان و توضیح دوره را کامل کنید.', ['type' => 'course', 'tab' => 'overview']); $this->check($checks, 'structure', $version->modules->isNotEmpty() && $version->lessons->isNotEmpty(), 'ساختار محتوا', 'ساختار دوره دارای ماژول و درس است.', 'حداقل یک ماژول و یک درس لازم است.', ['type' => 'course', 'tab' => 'content']); $emptyLessons = $version->lessons->filter(fn ($lesson) => $lesson->blocks()->count() === 0); $emptyLesson = $emptyLessons->first(); $this->check($checks, 'lesson_content', $emptyLessons->isEmpty(), 'محتوای درس‌ها', 'همه درس‌ها محتوا دارند.', $emptyLessons->isEmpty() ? '' : 'درس‌های بدون محتوا: '.$emptyLessons->pluck('title')->implode('، '), $emptyLesson ? ['type' => 'lesson', 'lessonId' => $emptyLesson->getKey()] : ['type' => 'course', 'tab' => 'content']); $invalidBlocks = []; foreach ($version->blocks as $block) { try { $this->blocks->validate($block->type, $block->schema_version, $block->data); } catch (ValidationException) { $invalidBlocks[] = ['id' => $block->getKey(), 'lessonId' => $block->lesson_id]; } } $invalidBlock = $invalidBlocks[0] ?? null; $this->check($checks, 'blocks', $invalidBlocks === [], 'اعتبار بلوک‌ها', 'همه بلوک‌ها معتبر هستند.', $invalidBlocks === [] ? '' : count($invalidBlocks).' بلوک نامعتبر است.', $invalidBlock ? ['type' => 'block', 'lessonId' => $invalidBlock['lessonId'], 'blockId' => $invalidBlock['id']] : ['type' => 'course', 'tab' => 'content']); $assetIds = $version->blocks->flatMap(fn ($block) => $this->assets->assetIds($block->data))->unique()->values(); $existingAssetIds = Asset::query()->where('organization_id', $version->organization_id)->whereIn('id', $assetIds)->pluck('id'); $missingAssetIds = $assetIds->diff($existingAssetIds); $assetBlock = $version->blocks->first(fn ($block) => collect($this->assets->assetIds($block->data))->intersect($missingAssetIds)->isNotEmpty()); $this->check($checks, 'assets', $missingAssetIds->isEmpty(), 'فایل‌های وابسته', 'همه فایل‌های استفاده‌شده در دسترس‌اند.', 'یک یا چند فایل استفاده‌شده حذف یا خارج از سازمان است.', $assetBlock ? ['type' => 'block', 'lessonId' => $assetBlock->lesson_id, 'blockId' => $assetBlock->getKey()] : ['type' => 'assets']); $invalidQuestions = []; foreach ($version->assessments->flatMap->questions as $question) { try { $this->questions->validate($question->type, $question->configuration); } catch (ValidationException) { $invalidQuestions[] = $question->getKey(); } } $emptyAssessments = $version->assessments->filter(fn ($assessment) => $assessment->questions->isEmpty()); $assessmentContentReady = $invalidQuestions === [] && $emptyAssessments->isEmpty(); $rules = $version->completion_rules ?? []; $rulesReady = isset($rules['mode'], $rules['rules']) && in_array($rules['mode'], ['all', 'any'], true) && is_array($rules['rules']) && $rules['rules'] !== []; $assessmentReady = $assessmentContentReady && $rulesReady; $assessmentFailure = ! $assessmentContentReady ? 'ارزیابی بدون سؤال یا سؤال/سناریوی نامعتبر وجود دارد.' : (! $rulesReady ? 'حداقل یک قانون تکمیل تعریف کنید.' : ''); $this->check($checks, 'assessments', $assessmentReady, 'ارزیابی‌ها و سناریوها', 'ارزیابی‌ها، سناریوها و قانون تکمیل معتبر هستند.', $assessmentFailure, $assessmentContentReady ? ['type' => 'completion'] : ['type' => 'assessments'], $assessmentContentReady && ! $rulesReady ? 'warning' : 'error'); return ['ready' => collect($checks)->every(fn (array $check): bool => $check['passed']), 'checks' => $checks]; } public function submitReview(CourseVersion $version): CourseVersion { if ($version->status !== CourseVersionStatus::Draft) { throw ValidationException::withMessages(['version' => ['فقط Draft را می‌توان برای بازبینی ارسال کرد.']]); } $readiness = $this->readiness($version); if (! $readiness['ready']) { throw ValidationException::withMessages(['readiness' => collect($readiness['checks'])->where('passed', false)->pluck('failure')->values()->all()]); } $version->update(['status' => CourseVersionStatus::InReview, 'review_submitted_at' => now()]); return $version->fresh(); } public function returnToDraft(CourseVersion $version): CourseVersion { if ($version->status !== CourseVersionStatus::InReview || $version->scheduled_publish_at) { throw ValidationException::withMessages(['version' => ['نسخه زمان‌بندی‌شده را ابتدا از زمان‌بندی خارج کنید.']]); } $version->update(['status' => CourseVersionStatus::Draft, 'review_submitted_at' => null]); return $version->fresh(); } /** @param array $completionRules */ public function configure(CourseVersion $version, array $completionRules): CourseVersion { if (! in_array($version->status, [CourseVersionStatus::Draft, CourseVersionStatus::InReview], true)) { throw ValidationException::withMessages(['version' => ['نسخه منتشرشده قابل تغییر نیست.']]); } $version->update(['completion_rules' => $completionRules]); return $version->fresh(); } public function publish(CourseVersion $version, ?string $publishedBy = null): CourseVersion { if ($version->status !== CourseVersionStatus::InReview) { throw ValidationException::withMessages(['version' => ['نسخه باید ابتدا در وضعیت بازبینی باشد.']]); } $readiness = $this->readiness($version); if (! $readiness['ready']) { throw ValidationException::withMessages(['readiness' => collect($readiness['checks'])->where('passed', false)->pluck('failure')->values()->all()]); } return DB::transaction(function () use ($version, $publishedBy): CourseVersion { $snapshot = ContentTaxonomyMapping::query()->with('taxonomyNode.type')->where('course_version_id', $version->getKey())->get()->map(fn (ContentTaxonomyMapping $mapping) => [ 'mappingId' => $mapping->getKey(), 'mappableType' => $mapping->mappable_type->value, 'mappableId' => $mapping->mappable_id, 'mappingType' => $mapping->mapping_type->value, 'weight' => (float) $mapping->weight, 'taxonomyNode' => ['id' => $mapping->taxonomy_node_id, 'name' => $mapping->taxonomyNode->name, 'code' => $mapping->taxonomyNode->code, 'type' => $mapping->taxonomyNode->type->key], ])->values()->all(); $version->update(['status' => CourseVersionStatus::Published, 'published_at' => now(), 'published_by' => $publishedBy, 'scheduled_publish_at' => null, 'taxonomy_snapshot' => $snapshot]); $version->course()->update(['status' => 'published']); return $version->fresh(); }); } public function schedule(CourseVersion $version, \DateTimeInterface $publishAt, ?\DateTimeInterface $unpublishAt): CourseVersion { if ($version->status !== CourseVersionStatus::InReview) { throw ValidationException::withMessages(['version' => ['فقط نسخه در حال بازبینی قابل زمان‌بندی است.']]); } if ($unpublishAt && $unpublishAt <= $publishAt) { throw ValidationException::withMessages(['scheduledUnpublishAt' => ['زمان توقف انتشار باید بعد از انتشار باشد.']]); } $version->update(['scheduled_publish_at' => $publishAt, 'scheduled_unpublish_at' => $unpublishAt]); return $version->fresh(); } public function cancelSchedule(CourseVersion $version): CourseVersion { if ($version->status !== CourseVersionStatus::InReview) { throw ValidationException::withMessages(['version' => ['زمان‌بندی این نسخه قابل لغو نیست.']]); } $version->update(['scheduled_publish_at' => null, 'scheduled_unpublish_at' => null]); return $version->fresh(); } public function unpublish(CourseVersion $version): void { if ($version->status !== CourseVersionStatus::Published || $version->unpublished_at) { throw ValidationException::withMessages(['version' => ['این نسخه منتشرشده و فعال نیست.']]); } DB::table('course_versions')->where('id', $version->getKey())->update(['unpublished_at' => now(), 'updated_at' => now()]); $hasActive = CourseVersion::query()->where('course_id', $version->course_id)->where('status', CourseVersionStatus::Published)->whereNull('unpublished_at')->exists(); if (! $hasActive) { Course::query()->whereKey($version->course_id)->update(['status' => 'archived']); } } /** @param list> $checks */ private function check(array &$checks, string $key, bool $passed, string $label, string $success, string $failure, array $target, string $failureStatus = 'error'): void { $status = $passed ? 'success' : $failureStatus; $checks[] = compact('key', 'passed', 'label', 'success', 'failure', 'status', 'target'); } }