761 lines
37 KiB
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,
|
|
]);
|
|
}
|
|
}
|
|
} |