Add membership fee system with disability discount and fix document permissions
Features: - Implement two fee types: entrance fee and annual fee (both NT$1,000) - Add 50% discount for disability certificate holders - Add disability certificate upload in member profile - Integrate disability verification into cashier approval workflow - Add membership fee settings in system admin Document permissions: - Fix hard-coded role logic in Document model - Use permission-based authorization instead of role checks Additional features: - Add announcements, general ledger, and trial balance modules - Add income management and accounting entries - Add comprehensive test suite with factories - Update UI translations to Traditional Chinese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
129
resources/views/admin/announcements/create.blade.php
Normal file
129
resources/views/admin/announcements/create.blade.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
建立公告
|
||||
</h2>
|
||||
<a href="{{ route('admin.announcements.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
← 返回列表
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<form method="POST" action="{{ route('admin.announcements.store') }}" class="space-y-6 p-6">
|
||||
@csrf
|
||||
|
||||
<!-- Title -->
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">標題 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="title" id="title" value="{{ old('title') }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||||
placeholder="輸入公告標題">
|
||||
@error('title')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">內容 <span class="text-red-500">*</span></label>
|
||||
<textarea name="content" id="content" rows="10" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||||
placeholder="輸入公告內容">{{ old('content') }}</textarea>
|
||||
@error('content')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Access Level -->
|
||||
<div>
|
||||
<label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限 <span class="text-red-500">*</span></label>
|
||||
<select name="access_level" id="access_level" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<option value="members" {{ old('access_level') === 'members' ? 'selected' : '' }}>會員(需付費會籍)</option>
|
||||
<option value="public" {{ old('access_level') === 'public' ? 'selected' : '' }}>公開(所有人可見)</option>
|
||||
<option value="board" {{ old('access_level') === 'board' ? 'selected' : '' }}>理事會(僅理事可見)</option>
|
||||
<option value="admin" {{ old('access_level') === 'admin' ? 'selected' : '' }}>管理員(僅管理員可見)</option>
|
||||
</select>
|
||||
@error('access_level')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Published At -->
|
||||
<div>
|
||||
<label for="published_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">發布時間(選填,留空則立即發布)</label>
|
||||
<input type="datetime-local" name="published_at" id="published_at" value="{{ old('published_at') }}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">設定未來時間可排程發布</p>
|
||||
@error('published_at')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Expires At -->
|
||||
<div>
|
||||
<label for="expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">過期時間(選填)</label>
|
||||
<input type="datetime-local" name="expires_at" id="expires_at" value="{{ old('expires_at') }}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">過期後將自動隱藏</p>
|
||||
@error('expires_at')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Is Pinned -->
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="is_pinned" id="is_pinned" value="1" {{ old('is_pinned') ? 'checked' : '' }}
|
||||
class="h-4 w-4 rounded border-gray-300 dark:border-gray-700 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-900">
|
||||
<label for="is_pinned" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">置頂此公告</label>
|
||||
</div>
|
||||
|
||||
<!-- Display Order (only shown when pinned) -->
|
||||
<div id="display_order_container" style="display: none;">
|
||||
<label for="display_order" class="block text-sm font-medium text-gray-700 dark:text-gray-300">顯示順序</label>
|
||||
<input type="number" name="display_order" id="display_order" value="{{ old('display_order', 0) }}" min="0"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">數字越小越優先顯示</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<a href="{{ route('admin.announcements.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
取消
|
||||
</a>
|
||||
<button type="submit" name="save_action" value="draft" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
儲存為草稿
|
||||
</button>
|
||||
<button type="submit" name="save_action" value="publish" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
||||
發布公告
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const isPinnedCheckbox = document.getElementById('is_pinned');
|
||||
const displayOrderContainer = document.getElementById('display_order_container');
|
||||
|
||||
function toggleDisplayOrder() {
|
||||
if (isPinnedCheckbox.checked) {
|
||||
displayOrderContainer.style.display = 'block';
|
||||
} else {
|
||||
displayOrderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
isPinnedCheckbox.addEventListener('change', toggleDisplayOrder);
|
||||
toggleDisplayOrder();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
126
resources/views/admin/announcements/edit.blade.php
Normal file
126
resources/views/admin/announcements/edit.blade.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
編輯公告
|
||||
</h2>
|
||||
<a href="{{ route('admin.announcements.show', $announcement) }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
← 返回查看
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<form method="POST" action="{{ route('admin.announcements.update', $announcement) }}" class="space-y-6 p-6">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
<!-- Title -->
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">標題 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="title" id="title" value="{{ old('title', $announcement->title) }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
@error('title')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">內容 <span class="text-red-500">*</span></label>
|
||||
<textarea name="content" id="content" rows="10" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">{{ old('content', $announcement->content) }}</textarea>
|
||||
@error('content')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Access Level -->
|
||||
<div>
|
||||
<label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限 <span class="text-red-500">*</span></label>
|
||||
<select name="access_level" id="access_level" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<option value="members" {{ old('access_level', $announcement->access_level) === 'members' ? 'selected' : '' }}>會員</option>
|
||||
<option value="public" {{ old('access_level', $announcement->access_level) === 'public' ? 'selected' : '' }}>公開</option>
|
||||
<option value="board" {{ old('access_level', $announcement->access_level) === 'board' ? 'selected' : '' }}>理事會</option>
|
||||
<option value="admin" {{ old('access_level', $announcement->access_level) === 'admin' ? 'selected' : '' }}>管理員</option>
|
||||
</select>
|
||||
@error('access_level')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Published At -->
|
||||
<div>
|
||||
<label for="published_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">發布時間</label>
|
||||
<input type="datetime-local" name="published_at" id="published_at"
|
||||
value="{{ old('published_at', $announcement->published_at?->format('Y-m-d\TH:i')) }}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
@error('published_at')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Expires At -->
|
||||
<div>
|
||||
<label for="expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">過期時間</label>
|
||||
<input type="datetime-local" name="expires_at" id="expires_at"
|
||||
value="{{ old('expires_at', $announcement->expires_at?->format('Y-m-d\TH:i')) }}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
@error('expires_at')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Is Pinned -->
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="is_pinned" id="is_pinned" value="1"
|
||||
{{ old('is_pinned', $announcement->is_pinned) ? 'checked' : '' }}
|
||||
class="h-4 w-4 rounded border-gray-300 dark:border-gray-700 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-900">
|
||||
<label for="is_pinned" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">置頂此公告</label>
|
||||
</div>
|
||||
|
||||
<!-- Display Order -->
|
||||
<div id="display_order_container">
|
||||
<label for="display_order" class="block text-sm font-medium text-gray-700 dark:text-gray-300">顯示順序</label>
|
||||
<input type="number" name="display_order" id="display_order"
|
||||
value="{{ old('display_order', $announcement->display_order) }}" min="0"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<a href="{{ route('admin.announcements.show', $announcement) }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
取消
|
||||
</a>
|
||||
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
||||
儲存變更
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const isPinnedCheckbox = document.getElementById('is_pinned');
|
||||
const displayOrderContainer = document.getElementById('display_order_container');
|
||||
|
||||
function toggleDisplayOrder() {
|
||||
if (isPinnedCheckbox.checked) {
|
||||
displayOrderContainer.style.display = 'block';
|
||||
} else {
|
||||
displayOrderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
isPinnedCheckbox.addEventListener('change', toggleDisplayOrder);
|
||||
toggleDisplayOrder();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
186
resources/views/admin/announcements/index.blade.php
Normal file
186
resources/views/admin/announcements/index.blade.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
公告管理
|
||||
</h2>
|
||||
<a href="{{ route('admin.announcements.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
||||
+ 建立公告
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
|
||||
@if (session('status'))
|
||||
<div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
|
||||
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-5">
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">總計</div>
|
||||
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{{ $stats['total'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">草稿</div>
|
||||
<div class="mt-2 text-3xl font-bold text-gray-600 dark:text-gray-400">{{ $stats['draft'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已發布</div>
|
||||
<div class="mt-2 text-3xl font-bold text-green-600 dark:text-green-400">{{ $stats['published'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已歸檔</div>
|
||||
<div class="mt-2 text-3xl font-bold text-yellow-600 dark:text-yellow-400">{{ $stats['archived'] }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">置頂中</div>
|
||||
<div class="mt-2 text-3xl font-bold text-blue-600 dark:text-blue-400">{{ $stats['pinned'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<form method="GET" action="{{ route('admin.announcements.index') }}" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">搜尋</label>
|
||||
<input type="text" name="search" id="search" value="{{ request('search') }}" placeholder="標題、內容..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">狀態</label>
|
||||
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<option value="">全部</option>
|
||||
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>草稿</option>
|
||||
<option value="published" {{ request('status') === 'published' ? 'selected' : '' }}>已發布</option>
|
||||
<option value="archived" {{ request('status') === 'archived' ? 'selected' : '' }}>已歸檔</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限</label>
|
||||
<select name="access_level" id="access_level" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<option value="">全部</option>
|
||||
<option value="public" {{ request('access_level') === 'public' ? 'selected' : '' }}>公開</option>
|
||||
<option value="members" {{ request('access_level') === 'members' ? 'selected' : '' }}>會員</option>
|
||||
<option value="board" {{ request('access_level') === 'board' ? 'selected' : '' }}>理事會</option>
|
||||
<option value="admin" {{ request('access_level') === 'admin' ? 'selected' : '' }}>管理員</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pinned" class="block text-sm font-medium text-gray-700 dark:text-gray-300">置頂</label>
|
||||
<select name="pinned" id="pinned" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
||||
<option value="">全部</option>
|
||||
<option value="yes" {{ request('pinned') === 'yes' ? 'selected' : '' }}>已置頂</option>
|
||||
<option value="no" {{ request('pinned') === 'no' ? 'selected' : '' }}>未置頂</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<a href="{{ route('admin.announcements.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
清除
|
||||
</a>
|
||||
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
||||
搜尋
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">共 {{ $announcements->total() }} 則公告</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Announcements Table -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">公告</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">狀態</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">存取權限</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">建立者</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">瀏覽次數</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">建立時間</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@forelse($announcements as $announcement)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
@if($announcement->is_pinned)
|
||||
<span class="mr-2 text-blue-500" title="置頂公告">📌</span>
|
||||
@endif
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<a href="{{ route('admin.announcements.show', $announcement) }}" class="hover:text-indigo-600 dark:hover:text-indigo-400">
|
||||
{{ $announcement->title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ \Illuminate\Support\Str::limit($announcement->content, 60) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5
|
||||
@if($announcement->status === 'draft') bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
|
||||
@elseif($announcement->status === 'published') bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
|
||||
@else bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300
|
||||
@endif">
|
||||
{{ $announcement->getStatusLabel() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $announcement->getAccessLevelLabel() }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $announcement->creator->name ?? 'N/A' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $announcement->view_count }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $announcement->created_at->format('Y-m-d H:i') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
||||
<a href="{{ route('admin.announcements.show', $announcement) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">查看</a>
|
||||
@if($announcement->canBeEditedBy(auth()->user()))
|
||||
<a href="{{ route('admin.announcements.edit', $announcement) }}" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300">編輯</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
沒有找到公告。<a href="{{ route('admin.announcements.create') }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">建立第一則公告</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($announcements->hasPages())
|
||||
<div class="bg-white dark:bg-gray-800 px-4 py-3 border-t border-gray-200 dark:border-gray-700 sm:px-6">
|
||||
{{ $announcements->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
170
resources/views/admin/announcements/show.blade.php
Normal file
170
resources/views/admin/announcements/show.blade.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
公告詳情
|
||||
</h2>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ route('admin.announcements.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
← 返回列表
|
||||
</a>
|
||||
@if($announcement->canBeEditedBy(auth()->user()))
|
||||
<a href="{{ route('admin.announcements.edit', $announcement) }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
||||
編輯公告
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-6">
|
||||
@if (session('status'))
|
||||
<div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
|
||||
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Announcement Content -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
@if($announcement->is_pinned)
|
||||
<span class="text-blue-500" title="置頂公告">📌</span>
|
||||
@endif
|
||||
{{ $announcement->title }}
|
||||
</h3>
|
||||
<span class="inline-flex rounded-full px-3 py-1 text-sm font-semibold
|
||||
@if($announcement->status === 'draft') bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
|
||||
@elseif($announcement->status === 'published') bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
|
||||
@else bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300
|
||||
@endif">
|
||||
{{ $announcement->getStatusLabel() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<div class="whitespace-pre-wrap text-gray-700 dark:text-gray-300">{{ $announcement->content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">公告資訊</h4>
|
||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">存取權限</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->getAccessLevelLabel() }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">瀏覽次數</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->view_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">建立者</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->creator->name ?? 'N/A' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">建立時間</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->created_at->format('Y-m-d H:i:s') }}</dd>
|
||||
</div>
|
||||
@if($announcement->published_at)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">發布時間</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
{{ $announcement->published_at->format('Y-m-d H:i:s') }}
|
||||
@if($announcement->isScheduled())
|
||||
<span class="ml-2 inline-flex rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-semibold text-blue-800 dark:text-blue-300">排程中</span>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if($announcement->expires_at)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">過期時間</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
{{ $announcement->expires_at->format('Y-m-d H:i:s') }}
|
||||
@if($announcement->isExpired())
|
||||
<span class="ml-2 inline-flex rounded-full bg-red-100 dark:bg-red-900/50 px-2 py-1 text-xs font-semibold text-red-800 dark:text-red-300">已過期</span>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if($announcement->lastUpdatedBy)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">最後更新者</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->lastUpdatedBy->name }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">最後更新時間</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->updated_at->format('Y-m-d H:i:s') }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
@if($announcement->canBeEditedBy(auth()->user()))
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">操作</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@if($announcement->isDraft() && auth()->user()->can('publish_announcements'))
|
||||
<form method="POST" action="{{ route('admin.announcements.publish', $announcement) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-green-600 dark:bg-green-500 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 dark:hover:bg-green-600">
|
||||
發布公告
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if($announcement->isPublished() && auth()->user()->can('publish_announcements'))
|
||||
<form method="POST" action="{{ route('admin.announcements.archive', $announcement) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
歸檔公告
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if(!$announcement->is_pinned && auth()->user()->can('edit_announcements'))
|
||||
<form method="POST" action="{{ route('admin.announcements.pin', $announcement) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
📌 置頂公告
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if($announcement->is_pinned && auth()->user()->can('edit_announcements'))
|
||||
<form method="POST" action="{{ route('admin.announcements.unpin', $announcement) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
取消置頂
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->can('delete_announcements'))
|
||||
<form method="POST" action="{{ route('admin.announcements.destroy', $announcement) }}"
|
||||
onsubmit="return confirm('確定要刪除此公告嗎?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-red-600 dark:bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 dark:hover:bg-red-600">
|
||||
刪除公告
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user