with('type') ->withCount('placements') ->when($request->string('type')->isNotEmpty(), fn ($query) => $query->whereHas('type', fn ($type) => $type->where('key', $request->string('type')->toString()))) ->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString())) ->when($request->string('search')->isNotEmpty(), fn ($query) => $query->where('title', 'like', '%'.$request->string('search')->toString().'%')) ->orderBy('title') ->paginate(30) ->withQueryString(); return view('content::admin.index', [ 'documents' => $documents, 'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(), ]); } public function create(Request $request): View { $types = ContentType::query()->where('is_active', true)->orderBy('name')->get(); $selectedType = $types->firstWhere('id', (int) $request->integer('type')) ?? $types->first(); return view('content::admin.form', [ 'document' => new ContentDocument(['status' => 'draft', 'data' => []]), 'types' => $types, 'selectedType' => $selectedType, ]); } public function store(Request $request): RedirectResponse { $validated = $this->validateDocument($request); $type = ContentType::query()->where('is_active', true)->findOrFail($validated['content_type_id']); $document = ContentDocument::query()->create([ 'content_type_id' => $type->id, 'title' => $validated['title'], 'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title']), 'status' => $validated['status'], 'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? now()) : null, 'expires_at' => $validated['expires_at'] ?? null, 'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR), 'created_by' => $request->user()?->id, 'updated_by' => $request->user()?->id, ]); return redirect()->route('content.documents.edit', $document)->with('status', 'Content document created.'); } public function edit(ContentDocument $document): View { $document->load('type', 'placements'); return view('content::admin.form', [ 'document' => $document, 'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(), 'selectedType' => $document->type, ]); } public function update(Request $request, ContentDocument $document): RedirectResponse { $validated = $this->validateDocument($request, $document); $document->update([ 'content_type_id' => $validated['content_type_id'], 'title' => $validated['title'], 'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title'], $document), 'status' => $validated['status'], 'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? $document->published_at ?? now()) : null, 'expires_at' => $validated['expires_at'] ?? null, 'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR), 'updated_by' => $request->user()?->id, ]); return back()->with('status', 'Content document updated.'); } public function duplicate(Request $request, ContentDocument $document): RedirectResponse { $copy = $document->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']); $copy->title = $document->title.' copy'; $copy->slug = $this->uniqueSlug($copy->title); $copy->status = 'draft'; $copy->published_at = null; $copy->created_by = $request->user()?->id; $copy->updated_by = $request->user()?->id; $copy->save(); return redirect()->route('content.documents.edit', $copy)->with('status', 'Independent draft copy created.'); } public function destroy(ContentDocument $document): RedirectResponse { if ($document->placements()->exists()) { return back()->withErrors(['document' => 'Remove this document from every page before deleting it.']); } $document->delete(); return redirect()->route('content.index')->with('status', 'Content document deleted.'); } private function validateDocument(Request $request, ?ContentDocument $document = null): array { return $request->validate([ 'content_type_id' => ['required', 'integer', Rule::exists('content_types', 'id')->where('is_active', true)], 'title' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255'], 'status' => ['required', Rule::in(['draft', 'published'])], 'published_at' => ['nullable', 'date'], 'expires_at' => ['nullable', 'date', 'after:published_at'], 'data_json' => ['required', 'json'], ]); } private function uniqueSlug(string $value, ?ContentDocument $except = null): string { $base = Str::slug($value) ?: 'content-document'; $slug = $base; $suffix = 2; while (ContentDocument::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) { $slug = $base.'-'.$suffix++; } return $slug; } }