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

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DocumentCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class DocumentCategoryController extends Controller
{
/**
* Display a listing of document categories
*/
public function index()
{
$categories = DocumentCategory::withCount('activeDocuments')
->orderBy('sort_order')
->get();
return view('admin.document-categories.index', compact('categories'));
}
/**
* Show the form for creating a new category
*/
public function create()
{
return view('admin.document-categories.create');
}
/**
* Store a newly created category
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'slug' => 'nullable|string|max:255|unique:document_categories,slug',
'description' => 'nullable|string',
'icon' => 'nullable|string|max:10',
'sort_order' => 'nullable|integer',
'default_access_level' => 'required|in:public,members,admin,board',
]);
// Auto-generate slug if not provided
if (empty($validated['slug'])) {
$validated['slug'] = Str::slug($validated['name']);
}
$category = DocumentCategory::create($validated);
return redirect()
->route('admin.document-categories.index')
->with('status', '文件類別已成功建立');
}
/**
* Show the form for editing a category
*/
public function edit(DocumentCategory $documentCategory)
{
return view('admin.document-categories.edit', compact('documentCategory'));
}
/**
* Update the specified category
*/
public function update(Request $request, DocumentCategory $documentCategory)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'slug' => 'nullable|string|max:255|unique:document_categories,slug,' . $documentCategory->id,
'description' => 'nullable|string',
'icon' => 'nullable|string|max:10',
'sort_order' => 'nullable|integer',
'default_access_level' => 'required|in:public,members,admin,board',
]);
$documentCategory->update($validated);
return redirect()
->route('admin.document-categories.index')
->with('status', '文件類別已成功更新');
}
/**
* Remove the specified category
*/
public function destroy(DocumentCategory $documentCategory)
{
// Check if category has documents
if ($documentCategory->documents()->count() > 0) {
return back()->with('error', '此類別下有文件,無法刪除');
}
$documentCategory->delete();
return redirect()
->route('admin.document-categories.index')
->with('status', '文件類別已成功刪除');
}
}

View File

@@ -0,0 +1,385 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AuditLog;
use App\Models\Document;
use App\Models\DocumentCategory;
use App\Models\DocumentVersion;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class DocumentController extends Controller
{
/**
* Display a listing of documents
*/
public function index(Request $request)
{
$query = Document::with(['category', 'currentVersion', 'createdBy'])
->orderBy('created_at', 'desc');
// Filter by category
if ($request->filled('category')) {
$query->where('document_category_id', $request->category);
}
// Filter by access level
if ($request->filled('access_level')) {
$query->where('access_level', $request->access_level);
}
// Filter by status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Search
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('document_number', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
$documents = $query->paginate(20);
$categories = DocumentCategory::orderBy('sort_order')->get();
return view('admin.documents.index', compact('documents', 'categories'));
}
/**
* Show the form for creating a new document
*/
public function create()
{
$categories = DocumentCategory::orderBy('sort_order')->get();
return view('admin.documents.create', compact('categories'));
}
/**
* Store a newly created document with initial version
*/
public function store(Request $request)
{
$validated = $request->validate([
'document_category_id' => 'required|exists:document_categories,id',
'title' => 'required|string|max:255',
'document_number' => 'nullable|string|max:255|unique:documents,document_number',
'description' => 'nullable|string',
'access_level' => 'required|in:public,members,admin,board',
'file' => 'required|file|max:10240', // 10MB max
'version_notes' => 'nullable|string',
]);
// Upload file
$file = $request->file('file');
$path = $file->store('documents', 'private');
// Create document
$document = Document::create([
'document_category_id' => $validated['document_category_id'],
'title' => $validated['title'],
'document_number' => $validated['document_number'],
'description' => $validated['description'],
'access_level' => $validated['access_level'],
'status' => 'active',
'created_by_user_id' => auth()->id(),
'version_count' => 0,
]);
// Add first version
$document->addVersion(
filePath: $path,
originalFilename: $file->getClientOriginalName(),
mimeType: $file->getMimeType(),
fileSize: $file->getSize(),
uploadedBy: auth()->user(),
versionNotes: $validated['version_notes'] ?? '初始版本'
);
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.created',
'description' => "建立文件:{$document->title}",
'ip_address' => request()->ip(),
]);
return redirect()
->route('admin.documents.show', $document)
->with('status', '文件已成功建立');
}
/**
* Display the specified document
*/
public function show(Document $document)
{
$document->load(['category', 'versions.uploadedBy', 'createdBy', 'lastUpdatedBy', 'accessLogs.user']);
$versionHistory = $document->getVersionHistory();
return view('admin.documents.show', compact('document', 'versionHistory'));
}
/**
* Show the form for editing the document metadata
*/
public function edit(Document $document)
{
$categories = DocumentCategory::orderBy('sort_order')->get();
return view('admin.documents.edit', compact('document', 'categories'));
}
/**
* Update the document metadata (not the file)
*/
public function update(Request $request, Document $document)
{
$validated = $request->validate([
'document_category_id' => 'required|exists:document_categories,id',
'title' => 'required|string|max:255',
'document_number' => 'nullable|string|max:255|unique:documents,document_number,' . $document->id,
'description' => 'nullable|string',
'access_level' => 'required|in:public,members,admin,board',
]);
$document->update([
...$validated,
'last_updated_by_user_id' => auth()->id(),
]);
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.updated',
'description' => "更新文件資訊:{$document->title}",
'ip_address' => request()->ip(),
]);
return redirect()
->route('admin.documents.show', $document)
->with('status', '文件資訊已成功更新');
}
/**
* Upload a new version of the document
*/
public function uploadNewVersion(Request $request, Document $document)
{
$validated = $request->validate([
'file' => 'required|file|max:10240', // 10MB max
'version_notes' => 'required|string',
]);
// Upload file
$file = $request->file('file');
$path = $file->store('documents', 'private');
// Add new version
$version = $document->addVersion(
filePath: $path,
originalFilename: $file->getClientOriginalName(),
mimeType: $file->getMimeType(),
fileSize: $file->getSize(),
uploadedBy: auth()->user(),
versionNotes: $validated['version_notes']
);
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.version_uploaded',
'description' => "上傳新版本:{$document->title} (版本 {$version->version_number})",
'ip_address' => request()->ip(),
]);
return back()->with('status', "新版本 {$version->version_number} 已成功上傳");
}
/**
* Promote an old version to current
*/
public function promoteVersion(Document $document, DocumentVersion $version)
{
if ($version->document_id !== $document->id) {
return back()->with('error', '版本不符合');
}
$document->promoteVersion($version, auth()->user());
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.version_promoted',
'description' => "提升版本為當前版本:{$document->title} (版本 {$version->version_number})",
'ip_address' => request()->ip(),
]);
return back()->with('status', "版本 {$version->version_number} 已設為當前版本");
}
/**
* Download a specific version
*/
public function downloadVersion(Document $document, DocumentVersion $version)
{
if ($version->document_id !== $document->id) {
abort(404);
}
if (!$version->fileExists()) {
abort(404, '檔案不存在');
}
// Log access
$document->logAccess('download', auth()->user());
return Storage::disk('private')->download(
$version->file_path,
$version->original_filename
);
}
/**
* Archive a document
*/
public function archive(Document $document)
{
$document->archive();
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.archived',
'description' => "封存文件:{$document->title}",
'ip_address' => request()->ip(),
]);
return back()->with('status', '文件已封存');
}
/**
* Restore an archived document
*/
public function restore(Document $document)
{
$document->unarchive();
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.restored',
'description' => "恢復文件:{$document->title}",
'ip_address' => request()->ip(),
]);
return back()->with('status', '文件已恢復');
}
/**
* Delete a document permanently
*/
public function destroy(Document $document)
{
$title = $document->title;
// Delete all version files
foreach ($document->versions as $version) {
if ($version->fileExists()) {
Storage::disk('private')->delete($version->file_path);
}
}
$document->delete();
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'document.deleted',
'description' => "刪除文件:{$title}",
'ip_address' => request()->ip(),
]);
return redirect()
->route('admin.documents.index')
->with('status', '文件已永久刪除');
}
/**
* Display document statistics dashboard
*/
public function statistics()
{
// Check if statistics feature is enabled
$settings = app(\App\Services\SettingsService::class);
if (!$settings->isFeatureEnabled('statistics')) {
abort(404, '統計功能未啟用');
}
// Check user permission
if (!auth()->user()->can('view_document_statistics')) {
abort(403, '您沒有檢視文件統計的權限');
}
$stats = [
'total_documents' => Document::where('status', 'active')->count(),
'total_versions' => \App\Models\DocumentVersion::count(),
'total_downloads' => Document::sum('download_count'),
'total_views' => Document::sum('view_count'),
'archived_documents' => Document::where('status', 'archived')->count(),
];
// Documents by category
$documentsByCategory = DocumentCategory::withCount(['activeDocuments'])
->orderBy('active_documents_count', 'desc')
->get();
// Most viewed documents
$mostViewed = Document::with(['category', 'currentVersion'])
->where('status', 'active')
->orderBy('view_count', 'desc')
->limit(10)
->get();
// Most downloaded documents
$mostDownloaded = Document::with(['category', 'currentVersion'])
->where('status', 'active')
->orderBy('download_count', 'desc')
->limit(10)
->get();
// Recent activity (last 30 days)
$recentActivity = \App\Models\DocumentAccessLog::with(['user', 'document'])
->where('accessed_at', '>=', now()->subDays(30))
->latest('accessed_at')
->limit(50)
->get();
// Monthly upload trends (last 6 months)
$uploadTrends = Document::selectRaw('DATE_FORMAT(created_at, "%Y-%m") as month, COUNT(*) as count')
->where('created_at', '>=', now()->subMonths(6))
->groupBy('month')
->orderBy('month', 'desc')
->get();
// Access level distribution
$accessLevelStats = Document::selectRaw('access_level, COUNT(*) as count')
->where('status', 'active')
->groupBy('access_level')
->get();
return view('admin.documents.statistics', compact(
'stats',
'documentsByCategory',
'mostViewed',
'mostDownloaded',
'recentActivity',
'uploadTrends',
'accessLevelStats'
));
}
}

View File

@@ -0,0 +1,273 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AuditLog;
use App\Models\SystemSetting;
use App\Services\SettingsService;
use Illuminate\Http\Request;
class SystemSettingsController extends Controller
{
protected $settings;
public function __construct(SettingsService $settings)
{
$this->settings = $settings;
}
/**
* Redirect to general settings
*/
public function index()
{
return redirect()->route('admin.settings.general');
}
/**
* Show general settings page
*/
public function general()
{
$settings = [
'system_name' => $this->settings->get('general.system_name', 'Usher Management System'),
'timezone' => $this->settings->get('general.timezone', 'Asia/Taipei'),
];
return view('admin.settings.general', compact('settings'));
}
/**
* Update general settings
*/
public function updateGeneral(Request $request)
{
$validated = $request->validate([
'system_name' => 'required|string|max:255',
'timezone' => 'required|string|max:255',
]);
SystemSetting::set('general.system_name', $validated['system_name'], 'string', 'general');
SystemSetting::set('general.timezone', $validated['timezone'], 'string', 'general');
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'settings.general.updated',
'description' => 'Updated general settings',
'ip_address' => $request->ip(),
]);
return redirect()->route('admin.settings.general')->with('status', '一般設定已更新');
}
/**
* Show document features settings page
*/
public function features()
{
$settings = [
'qr_codes_enabled' => $this->settings->isFeatureEnabled('qr_codes'),
'tagging_enabled' => $this->settings->isFeatureEnabled('tagging'),
'expiration_enabled' => $this->settings->isFeatureEnabled('expiration'),
'bulk_import_enabled' => $this->settings->isFeatureEnabled('bulk_import'),
'statistics_enabled' => $this->settings->isFeatureEnabled('statistics'),
'version_history_enabled' => $this->settings->isFeatureEnabled('version_history'),
];
return view('admin.settings.features', compact('settings'));
}
/**
* Update features settings
*/
public function updateFeatures(Request $request)
{
$features = [
'qr_codes_enabled',
'tagging_enabled',
'expiration_enabled',
'bulk_import_enabled',
'statistics_enabled',
'version_history_enabled',
];
foreach ($features as $feature) {
$value = $request->has($feature) ? true : false;
SystemSetting::set("features.{$feature}", $value, 'boolean', 'features');
}
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'settings.features.updated',
'description' => 'Updated document features settings',
'ip_address' => $request->ip(),
]);
return redirect()->route('admin.settings.features')->with('status', '功能設定已更新');
}
/**
* Show security & limits settings page
*/
public function security()
{
$settings = [
'rate_limit_authenticated' => $this->settings->getDownloadRateLimit(true),
'rate_limit_guest' => $this->settings->getDownloadRateLimit(false),
'max_file_size_mb' => $this->settings->getMaxFileSize(),
'allowed_file_types' => $this->settings->getAllowedFileTypes(),
];
return view('admin.settings.security', compact('settings'));
}
/**
* Update security settings
*/
public function updateSecurity(Request $request)
{
$validated = $request->validate([
'rate_limit_authenticated' => 'required|integer|min:1|max:1000',
'rate_limit_guest' => 'required|integer|min:1|max:1000',
'max_file_size_mb' => 'required|integer|min:1|max:100',
'allowed_file_types' => 'nullable|string',
]);
SystemSetting::set('security.rate_limit_authenticated', $validated['rate_limit_authenticated'], 'integer', 'security');
SystemSetting::set('security.rate_limit_guest', $validated['rate_limit_guest'], 'integer', 'security');
SystemSetting::set('security.max_file_size_mb', $validated['max_file_size_mb'], 'integer', 'security');
// Process allowed file types
if ($request->filled('allowed_file_types')) {
$types = array_map('trim', explode(',', $validated['allowed_file_types']));
SystemSetting::set('security.allowed_file_types', $types, 'json', 'security');
}
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'settings.security.updated',
'description' => 'Updated security and limits settings',
'ip_address' => $request->ip(),
]);
return redirect()->route('admin.settings.security')->with('status', '安全性設定已更新');
}
/**
* Show notifications settings page
*/
public function notifications()
{
$settings = [
'enabled' => $this->settings->areNotificationsEnabled(),
'expiration_alerts_enabled' => $this->settings->get('notifications.expiration_alerts_enabled', true),
'expiration_recipients' => $this->settings->getExpirationNotificationRecipients(),
'archive_notifications_enabled' => $this->settings->get('notifications.archive_notifications_enabled', true),
'new_document_alerts_enabled' => $this->settings->get('notifications.new_document_alerts_enabled', false),
];
return view('admin.settings.notifications', compact('settings'));
}
/**
* Update notifications settings
*/
public function updateNotifications(Request $request)
{
$validated = $request->validate([
'enabled' => 'boolean',
'expiration_alerts_enabled' => 'boolean',
'expiration_recipients' => 'nullable|string',
'archive_notifications_enabled' => 'boolean',
'new_document_alerts_enabled' => 'boolean',
]);
SystemSetting::set('notifications.enabled', $request->has('enabled'), 'boolean', 'notifications');
SystemSetting::set('notifications.expiration_alerts_enabled', $request->has('expiration_alerts_enabled'), 'boolean', 'notifications');
SystemSetting::set('notifications.archive_notifications_enabled', $request->has('archive_notifications_enabled'), 'boolean', 'notifications');
SystemSetting::set('notifications.new_document_alerts_enabled', $request->has('new_document_alerts_enabled'), 'boolean', 'notifications');
// Process email recipients
if ($request->filled('expiration_recipients')) {
$emails = array_map('trim', explode(',', $validated['expiration_recipients']));
$emails = array_filter($emails, fn($email) => filter_var($email, FILTER_VALIDATE_EMAIL));
SystemSetting::set('notifications.expiration_recipients', $emails, 'json', 'notifications');
} else {
SystemSetting::set('notifications.expiration_recipients', [], 'json', 'notifications');
}
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'settings.notifications.updated',
'description' => 'Updated notification settings',
'ip_address' => $request->ip(),
]);
return redirect()->route('admin.settings.notifications')->with('status', '通知設定已更新');
}
/**
* Show advanced settings page
*/
public function advanced()
{
$settings = [
'qr_code_size' => $this->settings->getQRCodeSize(),
'qr_code_format' => $this->settings->getQRCodeFormat(),
'statistics_time_range' => $this->settings->getStatisticsTimeRange(),
'statistics_top_n' => $this->settings->getStatisticsTopN(),
'audit_log_retention_days' => $this->settings->getAuditLogRetentionDays(),
'max_versions_retain' => $this->settings->getMaxVersionsToRetain(),
'default_expiration_days' => $this->settings->getDefaultExpirationDays(),
'expiration_warning_days' => $this->settings->getExpirationWarningDays(),
'auto_archive_enabled' => $this->settings->isAutoArchiveEnabled(),
'max_tags_per_document' => $this->settings->get('documents.max_tags_per_document', 10),
'default_access_level' => $this->settings->getDefaultAccessLevel(),
];
return view('admin.settings.advanced', compact('settings'));
}
/**
* Update advanced settings
*/
public function updateAdvanced(Request $request)
{
$validated = $request->validate([
'qr_code_size' => 'required|integer|min:100|max:1000',
'qr_code_format' => 'required|in:png,svg',
'statistics_time_range' => 'required|integer|min:7|max:365',
'statistics_top_n' => 'required|integer|min:5|max:100',
'audit_log_retention_days' => 'required|integer|min:30|max:3650',
'max_versions_retain' => 'required|integer|min:0|max:100',
'default_expiration_days' => 'required|integer|min:0|max:3650',
'expiration_warning_days' => 'required|integer|min:1|max:365',
'auto_archive_enabled' => 'boolean',
'max_tags_per_document' => 'required|integer|min:1|max:50',
'default_access_level' => 'required|in:public,members,admin,board',
]);
SystemSetting::set('advanced.qr_code_size', $validated['qr_code_size'], 'integer', 'advanced');
SystemSetting::set('advanced.qr_code_format', $validated['qr_code_format'], 'string', 'advanced');
SystemSetting::set('advanced.statistics_time_range', $validated['statistics_time_range'], 'integer', 'advanced');
SystemSetting::set('advanced.statistics_top_n', $validated['statistics_top_n'], 'integer', 'advanced');
SystemSetting::set('advanced.audit_log_retention_days', $validated['audit_log_retention_days'], 'integer', 'advanced');
SystemSetting::set('advanced.max_versions_retain', $validated['max_versions_retain'], 'integer', 'advanced');
SystemSetting::set('documents.default_expiration_days', $validated['default_expiration_days'], 'integer', 'documents');
SystemSetting::set('documents.expiration_warning_days', $validated['expiration_warning_days'], 'integer', 'documents');
SystemSetting::set('documents.auto_archive_enabled', $request->has('auto_archive_enabled'), 'boolean', 'documents');
SystemSetting::set('documents.max_tags_per_document', $validated['max_tags_per_document'], 'integer', 'documents');
SystemSetting::set('documents.default_access_level', $validated['default_access_level'], 'string', 'documents');
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'settings.advanced.updated',
'description' => 'Updated advanced settings',
'ip_address' => $request->ip(),
]);
return redirect()->route('admin.settings.advanced')->with('status', '進階設定已更新');
}
}