Files
SkinbaseNova/app/Http/Controllers/Settings/AcademyAdminController.php

761 lines
37 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\Academy\UpsertAcademyBadgeRequest;
use App\Http\Requests\Academy\UpsertAcademyCategoryRequest;
use App\Http\Requests\Academy\UpsertAcademyChallengeRequest;
use App\Http\Requests\Academy\UpsertAcademyLessonRequest;
use App\Http\Requests\Academy\UpsertAcademyPromptPackRequest;
use App\Http\Requests\Academy\UpsertAcademyPromptTemplateRequest;
use App\Models\AcademyBadge;
use App\Models\AcademyCategory;
use App\Models\AcademyChallenge;
use App\Models\AcademyChallengeSubmission;
use App\Models\AcademyLesson;
use App\Models\AcademyPromptPack;
use App\Models\AcademyPromptPackItem;
use App\Models\AcademyPromptTemplate;
use App\Services\Academy\AcademyCacheService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
final class AcademyAdminController extends Controller
{
public function __construct(private readonly AcademyCacheService $cache)
{
}
public function dashboard(): Response
{
return Inertia::render('Admin/Academy/Dashboard', [
'stats' => [
'lessons' => AcademyLesson::query()->count(),
'prompts' => AcademyPromptTemplate::query()->count(),
'packs' => AcademyPromptPack::query()->count(),
'challenges' => AcademyChallenge::query()->count(),
'submissions' => AcademyChallengeSubmission::query()->count(),
'badges' => AcademyBadge::query()->count(),
'creator_subscribers' => 0,
'pro_subscribers' => 0,
'mrr' => 0,
],
'links' => [
'categories' => route('admin.academy.categories.index'),
'lessons' => route('admin.academy.lessons.index'),
'prompts' => route('admin.academy.prompts.index'),
'packs' => route('admin.academy.packs.index'),
'challenges' => route('admin.academy.challenges.index'),
'submissions' => route('admin.academy.submissions.index'),
'badges' => route('admin.academy.badges.index'),
],
]);
}
public function categoriesIndex(): Response
{
return $this->renderIndex('categories');
}
public function categoriesCreate(): Response
{
return $this->renderForm('categories', new AcademyCategory());
}
public function categoriesStore(UpsertAcademyCategoryRequest $request): RedirectResponse
{
$category = new AcademyCategory();
$category->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.categories.edit', ['academyCategory' => $category])->with('success', 'Academy category created.');
}
public function categoriesEdit(AcademyCategory $academyCategory): Response
{
return $this->renderForm('categories', $academyCategory);
}
public function categoriesUpdate(UpsertAcademyCategoryRequest $request, AcademyCategory $academyCategory): RedirectResponse
{
$academyCategory->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.categories.edit', ['academyCategory' => $academyCategory])->with('success', 'Academy category updated.');
}
public function categoriesDestroy(AcademyCategory $academyCategory): RedirectResponse
{
$academyCategory->delete();
$this->cache->clearAll();
return redirect()->route('admin.academy.categories.index')->with('success', 'Academy category deleted.');
}
public function lessonsIndex(): Response
{
return $this->renderIndex('lessons');
}
public function lessonsCreate(): Response
{
return $this->renderForm('lessons', new AcademyLesson());
}
public function lessonsStore(UpsertAcademyLessonRequest $request): RedirectResponse
{
$lesson = new AcademyLesson();
$lesson->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.lessons.edit', ['academyLesson' => $lesson])->with('success', 'Academy lesson created.');
}
public function lessonsEdit(AcademyLesson $academyLesson): Response
{
return $this->renderForm('lessons', $academyLesson);
}
public function lessonsUpdate(UpsertAcademyLessonRequest $request, AcademyLesson $academyLesson): RedirectResponse
{
$academyLesson->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.lessons.edit', ['academyLesson' => $academyLesson])->with('success', 'Academy lesson updated.');
}
public function lessonsDestroy(AcademyLesson $academyLesson): RedirectResponse
{
$academyLesson->delete();
$this->cache->clearAll();
return redirect()->route('admin.academy.lessons.index')->with('success', 'Academy lesson deleted.');
}
public function promptsIndex(): Response
{
return $this->renderIndex('prompts');
}
public function promptsCreate(): Response
{
return $this->renderForm('prompts', new AcademyPromptTemplate());
}
public function promptsStore(UpsertAcademyPromptTemplateRequest $request): RedirectResponse
{
$prompt = new AcademyPromptTemplate();
$prompt->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt])->with('success', 'Academy prompt created.');
}
public function promptsEdit(AcademyPromptTemplate $academyPromptTemplate): Response
{
return $this->renderForm('prompts', $academyPromptTemplate);
}
public function promptsUpdate(UpsertAcademyPromptTemplateRequest $request, AcademyPromptTemplate $academyPromptTemplate): RedirectResponse
{
$academyPromptTemplate->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.prompts.edit', ['academyPromptTemplate' => $academyPromptTemplate])->with('success', 'Academy prompt updated.');
}
public function promptsDestroy(AcademyPromptTemplate $academyPromptTemplate): RedirectResponse
{
$academyPromptTemplate->delete();
$this->cache->clearAll();
return redirect()->route('admin.academy.prompts.index')->with('success', 'Academy prompt deleted.');
}
public function packsIndex(): Response
{
return $this->renderIndex('packs');
}
public function packsCreate(): Response
{
return $this->renderForm('packs', new AcademyPromptPack());
}
public function packsStore(UpsertAcademyPromptPackRequest $request): RedirectResponse
{
$pack = new AcademyPromptPack();
$pack->fill(collect($request->validated())->except('prompt_ids')->all())->save();
$this->syncPackItems($pack, $request->validated('prompt_ids', []));
$this->cache->clearAll();
return redirect()->route('admin.academy.packs.edit', ['academyPromptPack' => $pack])->with('success', 'Academy prompt pack created.');
}
public function packsEdit(AcademyPromptPack $academyPromptPack): Response
{
$academyPromptPack->load('prompts');
return $this->renderForm('packs', $academyPromptPack);
}
public function packsUpdate(UpsertAcademyPromptPackRequest $request, AcademyPromptPack $academyPromptPack): RedirectResponse
{
$academyPromptPack->fill(collect($request->validated())->except('prompt_ids')->all())->save();
$this->syncPackItems($academyPromptPack, $request->validated('prompt_ids', []));
$this->cache->clearAll();
return redirect()->route('admin.academy.packs.edit', ['academyPromptPack' => $academyPromptPack])->with('success', 'Academy prompt pack updated.');
}
public function packsDestroy(AcademyPromptPack $academyPromptPack): RedirectResponse
{
$academyPromptPack->delete();
$this->cache->clearAll();
return redirect()->route('admin.academy.packs.index')->with('success', 'Academy prompt pack deleted.');
}
public function challengesIndex(): Response
{
return $this->renderIndex('challenges');
}
public function challengesCreate(): Response
{
return $this->renderForm('challenges', new AcademyChallenge());
}
public function challengesStore(UpsertAcademyChallengeRequest $request): RedirectResponse
{
$challenge = new AcademyChallenge();
$challenge->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.challenges.edit', ['academyChallenge' => $challenge])->with('success', 'Academy challenge created.');
}
public function challengesEdit(AcademyChallenge $academyChallenge): Response
{
return $this->renderForm('challenges', $academyChallenge);
}
public function challengesUpdate(UpsertAcademyChallengeRequest $request, AcademyChallenge $academyChallenge): RedirectResponse
{
$academyChallenge->fill($request->validated())->save();
$this->cache->clearAll();
return redirect()->route('admin.academy.challenges.edit', ['academyChallenge' => $academyChallenge])->with('success', 'Academy challenge updated.');
}
public function challengesDestroy(AcademyChallenge $academyChallenge): RedirectResponse
{
$academyChallenge->delete();
$this->cache->clearAll();
return redirect()->route('admin.academy.challenges.index')->with('success', 'Academy challenge deleted.');
}
public function badgesIndex(): Response
{
return $this->renderIndex('badges');
}
public function badgesCreate(): Response
{
return $this->renderForm('badges', new AcademyBadge());
}
public function badgesStore(UpsertAcademyBadgeRequest $request): RedirectResponse
{
$badge = new AcademyBadge();
$badge->fill($request->validated())->save();
return redirect()->route('admin.academy.badges.edit', ['academyBadge' => $badge])->with('success', 'Academy badge created.');
}
public function badgesEdit(AcademyBadge $academyBadge): Response
{
return $this->renderForm('badges', $academyBadge);
}
public function badgesUpdate(UpsertAcademyBadgeRequest $request, AcademyBadge $academyBadge): RedirectResponse
{
$academyBadge->fill($request->validated())->save();
return redirect()->route('admin.academy.badges.edit', ['academyBadge' => $academyBadge])->with('success', 'Academy badge updated.');
}
public function badgesDestroy(AcademyBadge $academyBadge): RedirectResponse
{
$academyBadge->delete();
return redirect()->route('admin.academy.badges.index')->with('success', 'Academy badge deleted.');
}
public function submissionsIndex(Request $request): Response
{
$submissions = AcademyChallengeSubmission::query()
->with(['challenge', 'artwork', 'user'])
->latest('submitted_at')
->paginate(25)
->withQueryString();
$submissions->getCollection()->transform(fn (AcademyChallengeSubmission $submission): array => [
'id' => (int) $submission->id,
'moderation_status' => (string) $submission->moderation_status,
'submitted_at' => $submission->submitted_at?->toISOString(),
'ai_tool_used' => (string) ($submission->ai_tool_used ?? ''),
'prompt_used' => (string) ($submission->prompt_used ?? ''),
'workflow_notes' => (string) ($submission->workflow_notes ?? ''),
'challenge' => $submission->challenge ? [
'title' => (string) $submission->challenge->title,
'slug' => (string) $submission->challenge->slug,
] : null,
'user' => $submission->user ? [
'name' => (string) $submission->user->name,
'username' => (string) ($submission->user->username ?? ''),
] : null,
'artwork' => $submission->artwork ? [
'id' => (int) $submission->artwork->id,
'title' => (string) ($submission->artwork->title ?? 'Untitled artwork'),
'thumb_url' => $submission->artwork->thumbUrl('sm'),
] : null,
'approve_url' => route('admin.academy.submissions.approve', ['academyChallengeSubmission' => $submission]),
'reject_url' => route('admin.academy.submissions.reject', ['academyChallengeSubmission' => $submission]),
]);
return Inertia::render('Admin/Academy/Submissions', [
'submissions' => $submissions,
]);
}
public function approveSubmission(AcademyChallengeSubmission $academyChallengeSubmission): RedirectResponse
{
$academyChallengeSubmission->forceFill(['moderation_status' => 'approved'])->save();
return back()->with('success', 'Challenge submission approved.');
}
public function rejectSubmission(AcademyChallengeSubmission $academyChallengeSubmission): RedirectResponse
{
$academyChallengeSubmission->forceFill(['moderation_status' => 'rejected'])->save();
return back()->with('success', 'Challenge submission rejected.');
}
private function renderIndex(string $resource): Response
{
$meta = $this->resourceMeta($resource);
$items = $meta['model']::query()->latest('updated_at')->paginate(25)->withQueryString();
$items->getCollection()->transform(fn (Model $model): array => $this->serializeIndexItem($resource, $model));
return Inertia::render('Admin/Academy/CrudIndex', [
'resource' => $resource,
'title' => $meta['title'],
'subtitle' => $meta['subtitle'],
'items' => $items,
'columns' => $meta['columns'],
'createUrl' => route($meta['route_base'] . '.create'),
]);
}
private function renderForm(string $resource, Model $record): Response
{
$meta = $this->resourceMeta($resource);
return Inertia::render('Admin/Academy/CrudForm', [
'resource' => $resource,
'title' => $record->exists ? 'Edit ' . $meta['singular'] : 'Create ' . $meta['singular'],
'subtitle' => $meta['subtitle'],
'fields' => $meta['fields'],
'record' => $this->serializeFormRecord($resource, $record),
'submitUrl' => $record->exists ? route($meta['route_base'] . '.update', $this->routeParams($resource, $record)) : route($meta['route_base'] . '.store'),
'indexUrl' => route($meta['route_base'] . '.index'),
'destroyUrl' => $record->exists ? route($meta['route_base'] . '.destroy', $this->routeParams($resource, $record)) : null,
'method' => $record->exists ? 'patch' : 'post',
]);
}
private function resourceMeta(string $resource): array
{
return match ($resource) {
'categories' => [
'model' => AcademyCategory::class,
'title' => 'Academy Categories',
'singular' => 'category',
'subtitle' => 'Manage lesson, prompt, pack, and challenge categories.',
'route_base' => 'admin.academy.categories',
'columns' => ['name', 'type', 'slug', 'active'],
'fields' => [
['name' => 'type', 'label' => 'Type', 'type' => 'select', 'options' => [['value' => 'lesson', 'label' => 'Lesson'], ['value' => 'prompt', 'label' => 'Prompt'], ['value' => 'challenge', 'label' => 'Challenge'], ['value' => 'pack', 'label' => 'Pack']]],
['name' => 'name', 'label' => 'Name', 'type' => 'text'],
['name' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
['name' => 'icon', 'label' => 'Icon', 'type' => 'text'],
['name' => 'order_num', 'label' => 'Order', 'type' => 'number'],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
],
'lessons' => [
'model' => AcademyLesson::class,
'title' => 'Academy Lessons',
'singular' => 'lesson',
'subtitle' => 'Create and publish Academy lessons.',
'route_base' => 'admin.academy.lessons',
'columns' => ['title', 'difficulty', 'access_level', 'featured', 'active'],
'fields' => [
['name' => 'category_id', 'label' => 'Category', 'type' => 'select', 'options' => $this->categoryOptions('lesson')],
['name' => 'title', 'label' => 'Title', 'type' => 'text'],
['name' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['name' => 'excerpt', 'label' => 'Excerpt', 'type' => 'textarea'],
['name' => 'content', 'label' => 'Content', 'type' => 'textarea'],
['name' => 'difficulty', 'label' => 'Difficulty', 'type' => 'select', 'options' => $this->difficultyOptions()],
['name' => 'access_level', 'label' => 'Access', 'type' => 'select', 'options' => $this->accessOptions()],
['name' => 'lesson_type', 'label' => 'Lesson Type', 'type' => 'text'],
['name' => 'cover_image', 'label' => 'Cover Image', 'type' => 'text'],
['name' => 'video_url', 'label' => 'Video URL', 'type' => 'text'],
['name' => 'reading_minutes', 'label' => 'Reading Minutes', 'type' => 'number'],
['name' => 'published_at', 'label' => 'Published At', 'type' => 'datetime-local'],
['name' => 'seo_title', 'label' => 'SEO Title', 'type' => 'text'],
['name' => 'seo_description', 'label' => 'SEO Description', 'type' => 'textarea'],
['name' => 'featured', 'label' => 'Featured', 'type' => 'checkbox'],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
],
'prompts' => [
'model' => AcademyPromptTemplate::class,
'title' => 'Academy Prompt Templates',
'singular' => 'prompt template',
'subtitle' => 'Manage prompt previews, premium prompts, and prompt of the week.',
'route_base' => 'admin.academy.prompts',
'columns' => ['title', 'difficulty', 'access_level', 'prompt_of_week', 'active'],
'fields' => [
['name' => 'category_id', 'label' => 'Category', 'type' => 'select', 'options' => $this->categoryOptions('prompt')],
['name' => 'title', 'label' => 'Title', 'type' => 'text'],
['name' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['name' => 'excerpt', 'label' => 'Excerpt', 'type' => 'textarea'],
['name' => 'prompt', 'label' => 'Prompt', 'type' => 'textarea'],
['name' => 'negative_prompt', 'label' => 'Negative Prompt', 'type' => 'textarea'],
['name' => 'usage_notes', 'label' => 'Usage Notes', 'type' => 'textarea'],
['name' => 'workflow_notes', 'label' => 'Workflow Notes', 'type' => 'textarea'],
['name' => 'difficulty', 'label' => 'Difficulty', 'type' => 'select', 'options' => $this->difficultyOptions()],
['name' => 'access_level', 'label' => 'Access', 'type' => 'select', 'options' => $this->accessOptions()],
['name' => 'aspect_ratio', 'label' => 'Aspect Ratio', 'type' => 'text'],
['name' => 'tags', 'label' => 'Tags', 'type' => 'csv'],
['name' => 'preview_image', 'label' => 'Preview Image', 'type' => 'text'],
['name' => 'published_at', 'label' => 'Published At', 'type' => 'datetime-local'],
['name' => 'seo_title', 'label' => 'SEO Title', 'type' => 'text'],
['name' => 'seo_description', 'label' => 'SEO Description', 'type' => 'textarea'],
['name' => 'featured', 'label' => 'Featured', 'type' => 'checkbox'],
['name' => 'prompt_of_week', 'label' => 'Prompt Of Week', 'type' => 'checkbox'],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
],
'packs' => [
'model' => AcademyPromptPack::class,
'title' => 'Academy Prompt Packs',
'singular' => 'prompt pack',
'subtitle' => 'Bundle Academy prompts into reusable packs.',
'route_base' => 'admin.academy.packs',
'columns' => ['title', 'access_level', 'featured', 'active'],
'fields' => [
['name' => 'title', 'label' => 'Title', 'type' => 'text'],
['name' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['name' => 'excerpt', 'label' => 'Excerpt', 'type' => 'textarea'],
['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
['name' => 'access_level', 'label' => 'Access', 'type' => 'select', 'options' => $this->accessOptions()],
['name' => 'one_time_price_cents', 'label' => 'One-time Price (cents)', 'type' => 'number'],
['name' => 'currency', 'label' => 'Currency', 'type' => 'text'],
['name' => 'cover_image', 'label' => 'Cover Image', 'type' => 'text'],
['name' => 'tags', 'label' => 'Tags', 'type' => 'csv'],
['name' => 'prompt_ids', 'label' => 'Prompt Templates', 'type' => 'multiselect', 'options' => $this->promptOptions()],
['name' => 'published_at', 'label' => 'Published At', 'type' => 'datetime-local'],
['name' => 'featured', 'label' => 'Featured', 'type' => 'checkbox'],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
],
'challenges' => [
'model' => AcademyChallenge::class,
'title' => 'Academy Challenges',
'singular' => 'challenge',
'subtitle' => 'Create and moderate Academy challenge briefs.',
'route_base' => 'admin.academy.challenges',
'columns' => ['title', 'status', 'access_level', 'featured', 'active'],
'fields' => [
['name' => 'title', 'label' => 'Title', 'type' => 'text'],
['name' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['name' => 'excerpt', 'label' => 'Excerpt', 'type' => 'textarea'],
['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
['name' => 'brief', 'label' => 'Brief', 'type' => 'textarea'],
['name' => 'rules', 'label' => 'Rules', 'type' => 'textarea'],
['name' => 'access_level', 'label' => 'Access', 'type' => 'select', 'options' => $this->accessOptions()],
['name' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => [['value' => 'draft', 'label' => 'Draft'], ['value' => 'scheduled', 'label' => 'Scheduled'], ['value' => 'active', 'label' => 'Active'], ['value' => 'voting', 'label' => 'Voting'], ['value' => 'completed', 'label' => 'Completed'], ['value' => 'archived', 'label' => 'Archived']]],
['name' => 'starts_at', 'label' => 'Starts At', 'type' => 'datetime-local'],
['name' => 'ends_at', 'label' => 'Ends At', 'type' => 'datetime-local'],
['name' => 'voting_starts_at', 'label' => 'Voting Starts At', 'type' => 'datetime-local'],
['name' => 'voting_ends_at', 'label' => 'Voting Ends At', 'type' => 'datetime-local'],
['name' => 'cover_image', 'label' => 'Cover Image', 'type' => 'text'],
['name' => 'prize_text', 'label' => 'Prize Text', 'type' => 'text'],
['name' => 'required_tags', 'label' => 'Required Tags', 'type' => 'csv'],
['name' => 'allowed_categories', 'label' => 'Allowed Categories', 'type' => 'csv'],
['name' => 'featured', 'label' => 'Featured', 'type' => 'checkbox'],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
],
'badges' => [
'model' => AcademyBadge::class,
'title' => 'Academy Badges',
'singular' => 'badge',
'subtitle' => 'Define Academy plan and achievement badges.',
'route_base' => 'admin.academy.badges',
'columns' => ['name', 'badge_type', 'slug', 'active'],
'fields' => [
['name' => 'name', 'label' => 'Name', 'type' => 'text'],
['name' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['name' => 'description', 'label' => 'Description', 'type' => 'textarea'],
['name' => 'icon', 'label' => 'Icon', 'type' => 'text'],
['name' => 'badge_type', 'label' => 'Badge Type', 'type' => 'text'],
['name' => 'rules', 'label' => 'Rules JSON', 'type' => 'json'],
['name' => 'active', 'label' => 'Active', 'type' => 'checkbox'],
],
],
default => throw new \InvalidArgumentException('Unknown Academy resource [' . $resource . '].'),
};
}
private function serializeIndexItem(string $resource, Model $model): array
{
return match ($resource) {
'categories' => [
'id' => (int) $model->id,
'name' => (string) $model->name,
'type' => (string) $model->type,
'slug' => (string) $model->slug,
'active' => (bool) $model->active,
'edit_url' => route('admin.academy.categories.edit', ['academyCategory' => $model]),
'destroy_url' => route('admin.academy.categories.destroy', ['academyCategory' => $model]),
],
'lessons' => [
'id' => (int) $model->id,
'title' => (string) $model->title,
'difficulty' => (string) $model->difficulty,
'access_level' => (string) $model->access_level,
'featured' => (bool) $model->featured,
'active' => (bool) $model->active,
'edit_url' => route('admin.academy.lessons.edit', ['academyLesson' => $model]),
'destroy_url' => route('admin.academy.lessons.destroy', ['academyLesson' => $model]),
],
'prompts' => [
'id' => (int) $model->id,
'title' => (string) $model->title,
'difficulty' => (string) $model->difficulty,
'access_level' => (string) $model->access_level,
'prompt_of_week' => (bool) $model->prompt_of_week,
'active' => (bool) $model->active,
'edit_url' => route('admin.academy.prompts.edit', ['academyPromptTemplate' => $model]),
'destroy_url' => route('admin.academy.prompts.destroy', ['academyPromptTemplate' => $model]),
],
'packs' => [
'id' => (int) $model->id,
'title' => (string) $model->title,
'access_level' => (string) $model->access_level,
'featured' => (bool) $model->featured,
'active' => (bool) $model->active,
'edit_url' => route('admin.academy.packs.edit', ['academyPromptPack' => $model]),
'destroy_url' => route('admin.academy.packs.destroy', ['academyPromptPack' => $model]),
],
'challenges' => [
'id' => (int) $model->id,
'title' => (string) $model->title,
'status' => (string) $model->status,
'access_level' => (string) $model->access_level,
'featured' => (bool) $model->featured,
'active' => (bool) $model->active,
'edit_url' => route('admin.academy.challenges.edit', ['academyChallenge' => $model]),
'destroy_url' => route('admin.academy.challenges.destroy', ['academyChallenge' => $model]),
],
'badges' => [
'id' => (int) $model->id,
'name' => (string) $model->name,
'badge_type' => (string) $model->badge_type,
'slug' => (string) $model->slug,
'active' => (bool) $model->active,
'edit_url' => route('admin.academy.badges.edit', ['academyBadge' => $model]),
'destroy_url' => route('admin.academy.badges.destroy', ['academyBadge' => $model]),
],
default => [],
};
}
private function serializeFormRecord(string $resource, Model $record): array
{
return match ($resource) {
'categories' => [
'type' => (string) ($record->type ?? 'lesson'),
'name' => (string) ($record->name ?? ''),
'slug' => (string) ($record->slug ?? ''),
'description' => (string) ($record->description ?? ''),
'icon' => (string) ($record->icon ?? ''),
'order_num' => (int) ($record->order_num ?? 0),
'active' => (bool) ($record->active ?? true),
],
'lessons' => [
'category_id' => $record->category_id,
'title' => (string) ($record->title ?? ''),
'slug' => (string) ($record->slug ?? ''),
'excerpt' => (string) ($record->excerpt ?? ''),
'content' => (string) ($record->content ?? ''),
'difficulty' => (string) ($record->difficulty ?? 'beginner'),
'access_level' => (string) ($record->access_level ?? 'free'),
'lesson_type' => (string) ($record->lesson_type ?? 'article'),
'cover_image' => (string) ($record->cover_image ?? ''),
'video_url' => (string) ($record->video_url ?? ''),
'reading_minutes' => (int) ($record->reading_minutes ?? 5),
'published_at' => optional($record->published_at)?->format('Y-m-d\TH:i'),
'seo_title' => (string) ($record->seo_title ?? ''),
'seo_description' => (string) ($record->seo_description ?? ''),
'featured' => (bool) ($record->featured ?? false),
'active' => (bool) ($record->active ?? true),
],
'prompts' => [
'category_id' => $record->category_id,
'title' => (string) ($record->title ?? ''),
'slug' => (string) ($record->slug ?? ''),
'excerpt' => (string) ($record->excerpt ?? ''),
'prompt' => (string) ($record->prompt ?? ''),
'negative_prompt' => (string) ($record->negative_prompt ?? ''),
'usage_notes' => (string) ($record->usage_notes ?? ''),
'workflow_notes' => (string) ($record->workflow_notes ?? ''),
'difficulty' => (string) ($record->difficulty ?? 'beginner'),
'access_level' => (string) ($record->access_level ?? 'free'),
'aspect_ratio' => (string) ($record->aspect_ratio ?? ''),
'tags' => implode(', ', (array) ($record->tags ?? [])),
'preview_image' => (string) ($record->preview_image ?? ''),
'published_at' => optional($record->published_at)?->format('Y-m-d\TH:i'),
'seo_title' => (string) ($record->seo_title ?? ''),
'seo_description' => (string) ($record->seo_description ?? ''),
'featured' => (bool) ($record->featured ?? false),
'prompt_of_week' => (bool) ($record->prompt_of_week ?? false),
'active' => (bool) ($record->active ?? true),
],
'packs' => [
'title' => (string) ($record->title ?? ''),
'slug' => (string) ($record->slug ?? ''),
'excerpt' => (string) ($record->excerpt ?? ''),
'description' => (string) ($record->description ?? ''),
'access_level' => (string) ($record->access_level ?? 'creator'),
'one_time_price_cents' => $record->one_time_price_cents,
'currency' => (string) ($record->currency ?? 'EUR'),
'cover_image' => (string) ($record->cover_image ?? ''),
'tags' => implode(', ', (array) ($record->tags ?? [])),
'prompt_ids' => $record instanceof AcademyPromptPack ? $record->prompts->pluck('id')->all() : [],
'published_at' => optional($record->published_at)?->format('Y-m-d\TH:i'),
'featured' => (bool) ($record->featured ?? false),
'active' => (bool) ($record->active ?? true),
],
'challenges' => [
'title' => (string) ($record->title ?? ''),
'slug' => (string) ($record->slug ?? ''),
'excerpt' => (string) ($record->excerpt ?? ''),
'description' => (string) ($record->description ?? ''),
'brief' => (string) ($record->brief ?? ''),
'rules' => (string) ($record->rules ?? ''),
'access_level' => (string) ($record->access_level ?? 'free'),
'status' => (string) ($record->status ?? 'draft'),
'starts_at' => optional($record->starts_at)?->format('Y-m-d\TH:i'),
'ends_at' => optional($record->ends_at)?->format('Y-m-d\TH:i'),
'voting_starts_at' => optional($record->voting_starts_at)?->format('Y-m-d\TH:i'),
'voting_ends_at' => optional($record->voting_ends_at)?->format('Y-m-d\TH:i'),
'cover_image' => (string) ($record->cover_image ?? ''),
'prize_text' => (string) ($record->prize_text ?? ''),
'required_tags' => implode(', ', (array) ($record->required_tags ?? [])),
'allowed_categories' => implode(', ', (array) ($record->allowed_categories ?? [])),
'featured' => (bool) ($record->featured ?? false),
'active' => (bool) ($record->active ?? true),
],
'badges' => [
'name' => (string) ($record->name ?? ''),
'slug' => (string) ($record->slug ?? ''),
'description' => (string) ($record->description ?? ''),
'icon' => (string) ($record->icon ?? ''),
'badge_type' => (string) ($record->badge_type ?? 'achievement'),
'rules' => json_encode((array) ($record->rules ?? []), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
'active' => (bool) ($record->active ?? true),
],
default => [],
};
}
private function routeParams(string $resource, Model $record): array
{
return match ($resource) {
'categories' => ['academyCategory' => $record],
'lessons' => ['academyLesson' => $record],
'prompts' => ['academyPromptTemplate' => $record],
'packs' => ['academyPromptPack' => $record],
'challenges' => ['academyChallenge' => $record],
'badges' => ['academyBadge' => $record],
default => [],
};
}
private function categoryOptions(string $type): array
{
return AcademyCategory::query()
->where('type', $type)
->orderBy('order_num')
->orderBy('name')
->get()
->map(fn (AcademyCategory $category): array => ['value' => $category->id, 'label' => $category->name])
->prepend(['value' => '', 'label' => 'No category'])
->values()
->all();
}
private function promptOptions(): array
{
return AcademyPromptTemplate::query()
->orderBy('title')
->get()
->map(fn (AcademyPromptTemplate $prompt): array => ['value' => $prompt->id, 'label' => $prompt->title])
->values()
->all();
}
private function difficultyOptions(): array
{
return collect((array) config('academy.difficulty_levels', []))
->map(fn (string $value): array => ['value' => $value, 'label' => ucfirst($value)])
->values()
->all();
}
private function accessOptions(): array
{
return [
['value' => 'free', 'label' => 'Free'],
['value' => 'creator', 'label' => 'Creator'],
['value' => 'pro', 'label' => 'Pro'],
];
}
private function syncPackItems(AcademyPromptPack $pack, array $promptIds): void
{
AcademyPromptPackItem::query()->where('pack_id', $pack->id)->delete();
foreach (array_values($promptIds) as $index => $promptId) {
AcademyPromptPackItem::query()->create([
'pack_id' => $pack->id,
'prompt_template_id' => (int) $promptId,
'order_num' => $index,
]);
}
}
}