Initial commit
This commit is contained in:
446
app/Models/Document.php
Normal file
446
app/Models/Document.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Document extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'document_category_id',
|
||||
'title',
|
||||
'document_number',
|
||||
'description',
|
||||
'public_uuid',
|
||||
'access_level',
|
||||
'current_version_id',
|
||||
'status',
|
||||
'archived_at',
|
||||
'created_by_user_id',
|
||||
'last_updated_by_user_id',
|
||||
'view_count',
|
||||
'download_count',
|
||||
'version_count',
|
||||
'expires_at',
|
||||
'auto_archive_on_expiry',
|
||||
'expiry_notice',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'archived_at' => '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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user