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:
2025-12-01 09:56:01 +08:00
parent 83ce1f7fc8
commit 642b879dd4
207 changed files with 19487 additions and 3048 deletions

View 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>