diff --git a/app/Http/Controllers/Api/PublicDocumentController.php b/app/Http/Controllers/Api/PublicDocumentController.php new file mode 100644 index 0000000..b44f8ed --- /dev/null +++ b/app/Http/Controllers/Api/PublicDocumentController.php @@ -0,0 +1,89 @@ +with(['category', 'currentVersion.uploadedBy']) + ->where('status', 'active') + ->where('access_level', 'public') + ->whereNotNull('current_version_id') + ->orderByDesc('updated_at'); + + if ($request->filled('search')) { + $search = trim((string) $request->input('search')); + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%") + ->orWhere('document_number', 'like', "%{$search}%"); + }); + } + + if ($request->filled('category')) { + $category = (string) $request->input('category'); + + if (ctype_digit($category)) { + $query->where('document_category_id', (int) $category); + } else { + $query->whereHas('category', function ($q) use ($category) { + $q->where('slug', $category); + }); + } + } + + $perPage = min(max($request->integer('per_page', 100), 1), 500); + + return PublicDocumentCollectionResource::collection( + $query->paginate($perPage)->withQueryString() + ); + } + + /** + * Show a single public document with version history. + */ + public function show(string $uuid) + { + $document = Document::query() + ->with([ + 'category', + 'currentVersion.uploadedBy', + 'versions.uploadedBy', + 'versions.document', + 'createdBy', + 'lastUpdatedBy', + ]) + ->where('public_uuid', $uuid) + ->where('status', 'active') + ->where('access_level', 'public') + ->whereNotNull('current_version_id') + ->firstOrFail(); + + $related = Document::query() + ->with(['category', 'currentVersion.uploadedBy']) + ->where('status', 'active') + ->where('access_level', 'public') + ->whereNotNull('current_version_id') + ->where('id', '!=', $document->id) + ->where('document_category_id', $document->document_category_id) + ->orderByDesc('updated_at') + ->limit(4) + ->get(); + + return response()->json([ + 'data' => new PublicDocumentResource($document), + 'related' => PublicDocumentCollectionResource::collection($related), + ]); + } +} diff --git a/app/Http/Resources/PublicDocumentCollectionResource.php b/app/Http/Resources/PublicDocumentCollectionResource.php new file mode 100644 index 0000000..fc71dc1 --- /dev/null +++ b/app/Http/Resources/PublicDocumentCollectionResource.php @@ -0,0 +1,71 @@ + $this->id, + 'slug' => $this->public_uuid, + 'public_uuid' => $this->public_uuid, + 'title' => $this->title, + 'document_number' => $this->document_number, + 'summary' => $this->description, + 'description' => $this->description, + 'status' => $this->status, + 'status_label' => $this->getStatusLabel(), + 'access_level' => $this->access_level, + 'access_level_label' => $this->getAccessLevelLabel(), + 'published_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + 'expires_at' => $this->expires_at?->toDateString(), + 'version_count' => $this->version_count, + 'category' => $this->whenLoaded('category', function () { + return [ + 'id' => $this->category->id, + 'name' => $this->category->name, + 'slug' => $this->category->slug, + 'icon' => $this->category->icon, + ]; + }), + 'current_version' => $this->whenLoaded('currentVersion', function () { + if (! $this->currentVersion) { + return null; + } + + return [ + 'id' => $this->currentVersion->id, + 'version_number' => $this->currentVersion->version_number, + 'version_notes' => $this->currentVersion->version_notes, + 'is_current' => (bool) $this->currentVersion->is_current, + 'original_filename' => $this->currentVersion->original_filename, + 'mime_type' => $this->currentVersion->mime_type, + 'file_extension' => $this->currentVersion->getFileExtension(), + 'file_size' => $this->currentVersion->file_size, + 'file_size_human' => $this->currentVersion->getFileSizeHuman(), + 'file_hash' => $this->currentVersion->file_hash, + 'uploaded_by' => $this->currentVersion->uploadedBy?->name, + 'uploaded_at' => $this->currentVersion->uploaded_at?->toIso8601String(), + 'download_url' => route('documents.public.download', $this->public_uuid), + ]; + }), + 'links' => [ + 'api_url' => url('/api/v1/public-documents/'.$this->public_uuid), + 'detail_url' => route('documents.public.show', $this->public_uuid), + 'web_url' => route('documents.public.show', $this->public_uuid), + 'download_url' => route('documents.public.download', $this->public_uuid), + ], + 'metadata' => [ + 'document_type' => $this->category?->name, + 'expiration_status' => $this->getExpirationStatusLabel(), + 'auto_archive_on_expiry' => (bool) $this->auto_archive_on_expiry, + 'expiry_notice' => $this->expiry_notice, + ], + ]; + } +} diff --git a/app/Http/Resources/PublicDocumentResource.php b/app/Http/Resources/PublicDocumentResource.php new file mode 100644 index 0000000..9e6a0b4 --- /dev/null +++ b/app/Http/Resources/PublicDocumentResource.php @@ -0,0 +1,25 @@ +toArray($request), + [ + 'versions' => PublicDocumentVersionResource::collection($this->whenLoaded('versions')), + 'audit' => [ + 'view_count' => $this->view_count, + 'download_count' => $this->download_count, + 'last_updated_by' => $this->lastUpdatedBy?->name, + 'created_by' => $this->createdBy?->name, + ], + ] + ); + } +} diff --git a/app/Http/Resources/PublicDocumentVersionResource.php b/app/Http/Resources/PublicDocumentVersionResource.php new file mode 100644 index 0000000..c4cd104 --- /dev/null +++ b/app/Http/Resources/PublicDocumentVersionResource.php @@ -0,0 +1,31 @@ + $this->id, + 'version_number' => $this->version_number, + 'version_notes' => $this->version_notes, + 'is_current' => (bool) $this->is_current, + 'original_filename' => $this->original_filename, + 'mime_type' => $this->mime_type, + 'file_extension' => $this->getFileExtension(), + 'file_size' => $this->file_size, + 'file_size_human' => $this->getFileSizeHuman(), + 'file_hash' => $this->file_hash, + 'uploaded_by' => $this->uploadedBy?->name, + 'uploaded_at' => $this->uploaded_at?->toIso8601String(), + 'download_url' => route('documents.public.download-version', [ + 'uuid' => $this->document->public_uuid, + 'version' => $this->id, + ]), + ]; + } +} diff --git a/app/Observers/DocumentObserver.php b/app/Observers/DocumentObserver.php new file mode 100644 index 0000000..6d41956 --- /dev/null +++ b/app/Observers/DocumentObserver.php @@ -0,0 +1,54 @@ +revalidate($document); + } + + public function updated(Document $document): void + { + if (! $document->wasChanged(self::RELEVANT_FIELDS)) { + return; + } + + $this->revalidate($document); + } + + public function deleted(Document $document): void + { + $this->revalidate($document); + } + + public function restored(Document $document): void + { + $this->revalidate($document); + } + + private function revalidate(Document $document): void + { + SiteRevalidationService::revalidateDocument($document->public_uuid); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..99934fd 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Models\Document; +use App\Observers\DocumentObserver; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +21,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Document::observe(DocumentObserver::class); } } diff --git a/app/Services/SiteRevalidationService.php b/app/Services/SiteRevalidationService.php index b9d6cef..80a8f14 100644 --- a/app/Services/SiteRevalidationService.php +++ b/app/Services/SiteRevalidationService.php @@ -23,8 +23,20 @@ class SiteRevalidationService static::revalidate('page', $slug); } + /** + * Revalidate document cache on the Next.js frontend. + */ + public static function revalidateDocument(?string $slug = null): void + { + static::revalidate('document', $slug); + } + private static function revalidate(string $type, ?string $slug = null): void { + if (app()->runningUnitTests()) { + return; + } + $url = config('services.nextjs.revalidate_url'); $token = config('services.nextjs.revalidate_token'); diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index d23488a..a214f53 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -127,38 +127,46 @@ -