Initial commit

This commit is contained in:
2025-11-20 23:21:05 +08:00
commit 13bc6db529
378 changed files with 54527 additions and 0 deletions

446
app/Models/Document.php Normal file
View 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());
}
}