'datetime', 'expires_at' => 'date', 'auto_archive_on_expiry' => 'boolean', ]; protected static function boot() { parent::boot(); // Auto-generate UUID for public sharing static::creating(function ($document) { if (empty($document->public_uuid)) { $document->public_uuid = (string) Str::uuid(); } }); } // ==================== Relationships ==================== /** * Get the category this document belongs to */ public function category() { return $this->belongsTo(DocumentCategory::class, 'document_category_id'); } /** * Get all versions of this document */ public function versions() { return $this->hasMany(DocumentVersion::class)->orderBy('uploaded_at', 'desc'); } /** * Get the current published version */ public function currentVersion() { return $this->belongsTo(DocumentVersion::class, 'current_version_id'); } /** * Get the user who created this document */ public function createdBy() { return $this->belongsTo(User::class, 'created_by_user_id'); } /** * Get the user who last updated this document */ public function lastUpdatedBy() { return $this->belongsTo(User::class, 'last_updated_by_user_id'); } /** * Get the tags for this document */ public function tags() { return $this->belongsToMany(DocumentTag::class, 'document_document_tag') ->withTimestamps(); } /** * Get access logs for this document */ public function accessLogs() { return $this->hasMany(DocumentAccessLog::class)->orderBy('accessed_at', 'desc'); } // ==================== Status Check Methods ==================== /** * Check if document is active */ public function isActive(): bool { return $this->status === 'active'; } /** * Check if document is archived */ public function isArchived(): bool { return $this->status === 'archived'; } /** * Check if document is publicly accessible */ public function isPublic(): bool { return $this->access_level === 'public'; } /** * Check if document requires membership */ public function requiresMembership(): bool { return $this->access_level === 'members'; } /** * Check if document is admin-only */ public function isAdminOnly(): bool { return in_array($this->access_level, ['admin', 'board']); } // ==================== Version Control Methods ==================== /** * Add a new version to this document */ public function addVersion( string $filePath, string $originalFilename, string $mimeType, int $fileSize, User $uploadedBy, ?string $versionNotes = null ): DocumentVersion { // Calculate next version number $nextVersionNumber = $this->calculateNextVersionNumber(); // Unset current version flag on existing versions $this->versions()->update(['is_current' => false]); // Create new version $version = $this->versions()->create([ 'version_number' => $nextVersionNumber, 'version_notes' => $versionNotes, 'is_current' => true, 'file_path' => $filePath, 'original_filename' => $originalFilename, 'mime_type' => $mimeType, 'file_size' => $fileSize, 'file_hash' => hash_file('sha256', storage_path('app/' . $filePath)), 'uploaded_by_user_id' => $uploadedBy->id, 'uploaded_at' => now(), ]); // Update document's current_version_id and increment version count $this->update([ 'current_version_id' => $version->id, 'version_count' => $this->version_count + 1, 'last_updated_by_user_id' => $uploadedBy->id, ]); return $version; } /** * Calculate the next version number */ private function calculateNextVersionNumber(): string { $latestVersion = $this->versions()->orderBy('id', 'desc')->first(); if (!$latestVersion) { return '1.0'; } // Parse current version (e.g., "1.5" -> major: 1, minor: 5) $parts = explode('.', $latestVersion->version_number); $major = (int) ($parts[0] ?? 1); $minor = (int) ($parts[1] ?? 0); // Increment minor version $minor++; return "{$major}.{$minor}"; } /** * Promote an old version to be the current version */ public function promoteVersion(DocumentVersion $version, User $user): void { if ($version->document_id !== $this->id) { throw new \Exception('Version does not belong to this document'); } // Unset current flag on all versions $this->versions()->update(['is_current' => false]); // Set this version as current $version->update(['is_current' => true]); // Update document's current_version_id $this->update([ 'current_version_id' => $version->id, 'last_updated_by_user_id' => $user->id, ]); } /** * Get version history with comparison data */ public function getVersionHistory(): array { $versions = $this->versions()->with('uploadedBy')->get(); $history = []; foreach ($versions as $index => $version) { $previousVersion = $versions->get($index + 1); $history[] = [ 'version' => $version, 'size_change' => $previousVersion ? $version->file_size - $previousVersion->file_size : 0, 'days_since_previous' => $previousVersion ? $version->uploaded_at->diffInDays($previousVersion->uploaded_at) : null, ]; } return $history; } // ==================== Access Control Methods ==================== /** * Check if a user can view this document */ public function canBeViewedBy(?User $user): bool { if ($this->isPublic()) { return true; } if (!$user) { return false; } if ($user->is_admin || $user->hasRole('admin')) { return true; } if ($this->access_level === 'members') { return $user->member && $user->member->hasPaidMembership(); } if ($this->access_level === 'board') { return $user->hasRole(['admin', 'chair', 'board']); } return false; } /** * Log access to this document */ public function logAccess(string $action, ?User $user = null): void { $this->accessLogs()->create([ 'document_version_id' => $this->current_version_id, 'action' => $action, 'user_id' => $user?->id, 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent(), 'accessed_at' => now(), ]); // Increment counters if ($action === 'view') { $this->increment('view_count'); } elseif ($action === 'download') { $this->increment('download_count'); } } // ==================== Helper Methods ==================== /** * Get the public URL for this document */ public function getPublicUrl(): string { return route('documents.public.show', $this->public_uuid); } /** * Get the access level label in Chinese */ public function getAccessLevelLabel(): string { return match($this->access_level) { 'public' => '公開', 'members' => '會員', 'admin' => '管理員', 'board' => '理事會', default => '未知', }; } /** * Get status label in Chinese */ public function getStatusLabel(): string { return match($this->status) { 'active' => '啟用', 'archived' => '封存', default => '未知', }; } /** * Archive this document */ public function archive(): void { $this->update([ 'status' => 'archived', 'archived_at' => now(), ]); } /** * Restore archived document */ public function unarchive(): void { $this->update([ 'status' => 'active', 'archived_at' => null, ]); } /** * Check if document is expired */ public function isExpired(): bool { if (!$this->expires_at) { return false; } return $this->expires_at->isPast(); } /** * Check if document is expiring soon (within 30 days) */ public function isExpiringSoon(int $days = 30): bool { if (!$this->expires_at) { return false; } return $this->expires_at->isFuture() && $this->expires_at->diffInDays(now()) <= $days; } /** * Get expiration status label */ public function getExpirationStatusLabel(): ?string { if (!$this->expires_at) { return null; } if ($this->isExpired()) { return '已過期'; } if ($this->isExpiringSoon(7)) { return '即將過期'; } if ($this->isExpiringSoon(30)) { return '接近過期'; } return '有效'; } /** * Generate QR code for this document */ public function generateQRCode(?int $size = null, ?string $format = null): string { $settings = app(\App\Services\SettingsService::class); $size = $size ?? $settings->getQRCodeSize(); $format = $format ?? $settings->getQRCodeFormat(); return \SimpleSoftwareIO\QrCode\Facades\QrCode::size($size) ->format($format) ->generate($this->getPublicUrl()); } /** * Generate QR code as PNG */ public function generateQRCodePNG(?int $size = null): string { $settings = app(\App\Services\SettingsService::class); $size = $size ?? $settings->getQRCodeSize(); return \SimpleSoftwareIO\QrCode\Facades\QrCode::size($size) ->format('png') ->generate($this->getPublicUrl()); } }