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

359 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 space-y-6">
{{-- Pending Payment Alert for Admin --}}
@php
$approvedPayment = $member->payments()
->where('status', \App\Models\MembershipPayment::STATUS_APPROVED_CHAIR)
->latest()
->first();
@endphp
@if($approvedPayment && $member->isPending() && (Auth::user()->can('activate_memberships') || Auth::user()->hasRole('admin')))
<div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-green-800 dark:text-green-200">
準備啟用
</h3>
<div class="mt-2 text-sm text-green-700 dark:text-green-300">
<p>此會員的付款已完全核准,準備啟用會員資格。</p>
</div>
<div class="mt-4">
<a href="{{ route('admin.members.activate', $member) }}" class="inline-flex items-center rounded-md bg-green-600 dark:bg-green-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 dark:hover:bg-green-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600">
啟用會員資格
</a>
</div>
</div>
</div>
</div>
@endif
<section aria-labelledby="member-info-heading" class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
@if ($member->user?->profilePhotoUrl())
<img src="{{ $member->user->profilePhotoUrl() }}" alt="個人照片" class="h-16 w-16 rounded-full object-cover ring-2 ring-indigo-500">
@endif
<div>
<h3 id="member-info-heading" class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
{{ $member->full_name }}
</h3>
<div class="mt-1 flex items-center gap-2">
@php
$statusClasses = match($member->membership_status) {
'pending' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
'active' => 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'expired' => 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
'suspended' => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
default => 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
};
@endphp
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 {{ $statusClasses }}">
{{ $member->membership_status_label }}
</span>
<span class="inline-flex items-center rounded-md bg-gray-100 dark:bg-gray-700 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:text-gray-200">
{{ $member->membership_type_label }}
</span>
</div>
</div>
</div>
<a href="{{ route('admin.members.edit', $member) }}" class="inline-flex items-center 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 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
編輯
</a>
</div>
<dl class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2">
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6">
<dt class="truncate 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">
{{ $member->email }}
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6">
<dt class="truncate 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">
{{ $member->phone ?? __('Not set') }}
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6">
<dt class="truncate 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">
{{ $member->membership_status_label }}
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6">
<dt class="truncate 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">
{{ $member->membership_type_label }}
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6">
<dt class="truncate 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">
@if ($member->membership_started_at)
{{ $member->membership_started_at->toDateString() }}
@else
未設定
@endif
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6">
<dt class="truncate 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">
@if ($member->membership_expires_at)
{{ $member->membership_expires_at->toDateString() }}
@else
未設定
@endif
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6 sm:col-span-2">
<dt class="truncate 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 space-y-1">
<div>{{ $member->address_line_1 ?? __('Not set') }}</div>
@if ($member->address_line_2)
<div>{{ $member->address_line_2 }}</div>
@endif
<div>
{{ $member->city }}
@if ($member->postal_code)
, {{ $member->postal_code }}
@endif
</div>
</dd>
</div>
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6 sm:col-span-2">
<dt class="truncate 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 space-y-1">
<div>{{ $member->emergency_contact_name ?? __('Not set') }}</div>
<div>{{ $member->emergency_contact_phone ?? '' }}</div>
</dd>
</div>
{{-- Disability Certificate Status --}}
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:p-6 sm:col-span-2">
<dt class="truncate 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">
<div class="flex items-center gap-3">
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {{ $member->disability_status_badge }}">
{{ $member->disability_status_label }}
</span>
@if($member->hasDisabilityCertificate())
<a href="{{ route('admin.members.disability-certificate', $member) }}" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline text-sm">
檢視手冊
</a>
@endif
</div>
@if($member->hasApprovedDisability() && $member->disability_verified_at)
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
審核時間:{{ $member->disability_verified_at->format('Y/m/d H:i') }}
@if($member->disabilityVerifiedBy)
(審核人:{{ $member->disabilityVerifiedBy->name }}
@endif
</p>
@endif
@if($member->isDisabilityRejected() && $member->disability_rejection_reason)
<p class="mt-2 text-xs text-red-500 dark:text-red-400">
駁回原因:{{ $member->disability_rejection_reason }}
</p>
@endif
</dd>
</div>
</dl>
</div>
</section>
@if ($member->user)
<section aria-labelledby="roles-heading" class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<div class="flex items-center justify-between">
<h3 id="roles-heading" class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
角色管理
</h3>
</div>
<div class="flex flex-wrap gap-2">
@forelse ($member->user->roles as $role)
<span class="inline-flex items-center rounded-full bg-indigo-100 dark:bg-indigo-900 px-3 py-1 text-sm font-medium text-indigo-800 dark:text-indigo-200">
{{ $role->name }}
</span>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400">未指派角色。</p>
@endforelse
</div>
<form method="POST" action="{{ route('admin.members.roles.update', $member) }}" class="space-y-3">
@csrf
@method('PATCH')
<p class="text-sm text-gray-600 dark:text-gray-400">
選擇此會員使用者帳戶的角色。
</p>
<div class="grid gap-3 sm:grid-cols-2">
@foreach ($roles as $role)
<label class="flex items-center space-x-2">
<input
type="checkbox"
name="roles[]"
value="{{ $role->name }}"
@checked($member->user->hasRole($role->name))
class="rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:bg-gray-700"
>
<span class="text-sm text-gray-800 dark:text-gray-200">
{{ $role->name }}
<span class="block text-xs text-gray-500 dark:text-gray-400">{{ $role->description }}</span>
</span>
</label>
@endforeach
</div>
<div class="flex justify-end">
<button type="submit" class="inline-flex items-center 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 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
更新角色
</button>
</div>
</form>
</div>
</section>
@endif
<section aria-labelledby="admin-payment-history-heading" class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between">
<h3 id="admin-payment-history-heading" class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
付款歷史
</h3>
<a href="{{ route('admin.members.payments.create', $member) }}" class="inline-flex items-center 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 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
記錄付款
</a>
</div>
<div class="mt-5 overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" role="table">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
付款時間
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
金額
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
方式
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
狀態
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
提交者
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
操作
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($member->payments as $payment)
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ optional($payment->paid_at)->toDateString() }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
TWD {{ number_format($payment->amount, 0) }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $payment->payment_method_label ?? ($payment->method ?? __('N/A')) }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
{!! $payment->status_label ?? '<span class="inline-flex items-center rounded-md bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-300">' . __('Legacy') . '</span>' !!}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
@if($payment->submittedBy)
<div class="flex items-center">
<svg class="mr-1.5 h-4 w-4 text-blue-500" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" />
</svg>
<span class="text-xs">{{ $payment->submittedBy->name }}</span>
</div>
@else
<span class="text-xs text-gray-500 dark:text-gray-400">管理員</span>
@endif
</td>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm space-x-3">
@if($payment->status)
{{-- New payment verification system --}}
@if(Auth::user()->can('view_payment_verifications') || Auth::user()->hasRole('admin'))
<a href="{{ route('admin.payment-verifications.show', $payment) }}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
驗證
</a>
@endif
@if($payment->receipt_path)
<a href="{{ route('admin.payment-verifications.download-receipt', $payment) }}" class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300" title="下載收據">
收據
</a>
@endif
@else
{{-- Legacy admin-created payments --}}
<a href="{{ route('admin.members.payments.receipt', [$member, $payment]) }}" class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300" title="下載收據">
收據
</a>
<a href="{{ route('admin.members.payments.edit', [$member, $payment]) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
編輯
</a>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
<p class="mt-2">找不到付款記錄。</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</x-app-layout>