Files
Gbanyan 642b879dd4 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>
2025-12-01 09:56:01 +08:00

336 lines
24 KiB
PHP

<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
管理後台
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="space-y-6">
{{-- My Pending Approvals Alert --}}
@if ($myPendingApprovals > 0)
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/50 p-4" role="alert" aria-live="polite">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400 dark:text-yellow-300" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
您有 {{ $myPendingApprovals }} 個報銷單等待您的核准。
<a href="{{ route('admin.finance.index') }}" class="font-semibold underline hover:text-yellow-700 dark:hover:text-yellow-100">
查看待核准項目
</a>
</p>
</div>
</div>
</div>
@endif
{{-- Stats Grid --}}
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
{{-- Total Members --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">會員總數</dt>
<dd class="text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($totalMembers) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm">
<a href="{{ route('admin.members.index') }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看全部
</a>
</div>
</div>
</div>
{{-- Active Members --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-400 dark:text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">使用中會員</dt>
<dd class="text-2xl font-semibold text-green-600 dark:text-green-400">{{ number_format($activeMembers) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm">
<a href="{{ route('admin.members.index', ['status' => 'active']) }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看使用中
</a>
</div>
</div>
</div>
{{-- Expired Members --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-red-400 dark:text-red-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">已過期會員</dt>
<dd class="text-2xl font-semibold text-red-600 dark:text-red-400">{{ number_format($expiredMembers) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm">
<a href="{{ route('admin.members.index', ['status' => 'expired']) }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看已過期
</a>
</div>
</div>
</div>
{{-- Expiring Soon --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-400 dark:text-yellow-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">30天內到期</dt>
<dd class="text-2xl font-semibold text-yellow-600 dark:text-yellow-400">{{ number_format($expiringSoon) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm">
<span class="text-gray-500 dark:text-gray-400">需要更新提醒</span>
</div>
</div>
</div>
</div>
{{-- Revenue Stats --}}
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{{-- Total Revenue --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">總收入</dt>
<dd class="text-2xl font-semibold text-gray-900 dark:text-gray-100">${{ number_format($totalRevenue, 2) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ number_format($totalPayments) }} 總付款
</div>
</div>
</div>
{{-- This Month Revenue --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-400 dark:text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">本月</dt>
<dd class="text-2xl font-semibold text-green-600 dark:text-green-400">${{ number_format($revenueThisMonth, 2) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ number_format($paymentsThisMonth) }} 本月付款
</div>
</div>
</div>
{{-- Pending Approvals --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-400 dark:text-blue-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">報銷單</dt>
<dd class="text-2xl font-semibold text-blue-600 dark:text-blue-400">{{ number_format($pendingApprovals) }}</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm">
<a href="{{ route('admin.finance.index') }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看待處理
</a>
</div>
</div>
</div>
</div>
{{-- Recent Payments & Finance Stats --}}
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
{{-- Recent Payments --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">最近付款</h3>
<div class="mt-4 flow-root">
@if ($recentPayments->count() > 0)
<ul role="list" class="-my-5 divide-y divide-gray-200 dark:divide-gray-700">
@foreach ($recentPayments as $payment)
<li class="py-4">
<div class="flex items-center space-x-4">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $payment->member?->full_name ?? __('N/A') }}
</p>
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
{{ $payment->paid_at?->format('Y-m-d') ?? __('N/A') }}
</p>
</div>
<div>
<span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:text-green-200">
${{ number_format($payment->amount, 2) }}
</span>
</div>
</div>
</li>
@endforeach
</ul>
@else
<p class="text-sm text-gray-500 dark:text-gray-400">沒有最近的付款記錄。</p>
@endif
</div>
</div>
</div>
{{-- Finance Document Stats --}}
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">報銷單狀態</h3>
<div class="mt-6 space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="flex h-3 w-3 rounded-full bg-yellow-400"></span>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-100">待核准</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ number_format($pendingApprovals) }}</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="flex h-3 w-3 rounded-full bg-green-400"></span>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-100">完全核准</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ number_format($fullyApprovedDocs) }}</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="flex h-3 w-3 rounded-full bg-red-400"></span>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-100">已拒絕</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ number_format($rejectedDocs) }}</span>
</div>
</div>
</div>
</div>
</div>
{{-- Recent Announcements --}}
@if($recentAnnouncements->isNotEmpty())
<div class="mt-8">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="border-b border-gray-200 dark:border-gray-700 px-4 py-5 sm:px-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">📢 最新公告</h3>
@can('view_announcements')
<a href="{{ route('admin.announcements.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看全部
</a>
@endcan
</div>
</div>
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($recentAnnouncements as $announcement)
<li class="px-4 py-4 sm:px-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center mb-1">
@if($announcement->is_pinned)
<span class="mr-2 text-blue-500" title="置頂公告">📌</span>
@endif
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
{{ $announcement->title }}
</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{{ $announcement->getExcerpt(120) }}
</p>
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<span>{{ $announcement->published_at?->diffForHumans() ?? $announcement->created_at->diffForHumans() }}</span>
<span></span>
<span>{{ $announcement->getAccessLevelLabel() }}</span>
@if($announcement->view_count > 0)
<span></span>
<span>👁 {{ $announcement->view_count }} 次瀏覽</span>
@endif
</div>
</div>
@can('view_announcements')
<a href="{{ route('admin.announcements.show', $announcement) }}" class="ml-4 flex-shrink-0 text-sm font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看
</a>
@endcan
</div>
</li>
@endforeach
</ul>
</div>
</div>
@endif
</div>
</div>
</div>
</x-app-layout>