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

@@ -1,10 +1,10 @@
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Delete Account') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
</p>
</header>
@@ -19,11 +19,11 @@
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>

View File

@@ -0,0 +1,115 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
身心障礙手冊
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
持有身心障礙手冊的會員可享會費 50% 優惠。上傳後將在繳費審核時一併審核。
</p>
</header>
<!-- Current Status -->
<div class="mt-6 p-4 rounded-lg {{ $member->hasDisabilityCertificate() ? ($member->hasApprovedDisability() ? 'bg-green-50 dark:bg-green-900/30' : ($member->isDisabilityRejected() ? 'bg-red-50 dark:bg-red-900/30' : 'bg-yellow-50 dark:bg-yellow-900/30')) : 'bg-gray-50 dark:bg-gray-700/30' }}">
<div class="flex items-center">
@if($member->hasDisabilityCertificate())
@if($member->hasApprovedDisability())
<svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="ml-2 text-sm font-medium text-green-800 dark:text-green-200">審核通過</span>
@elseif($member->isDisabilityRejected())
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="ml-2 text-sm font-medium text-red-800 dark:text-red-200">審核未通過</span>
@else
<svg class="h-5 w-5 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="ml-2 text-sm font-medium text-yellow-800 dark:text-yellow-200">審核中</span>
@endif
@else
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="ml-2 text-sm font-medium text-gray-600 dark:text-gray-400">尚未上傳</span>
@endif
</div>
@if($member->isDisabilityRejected() && $member->disability_rejection_reason)
<div class="mt-3 text-sm text-red-700 dark:text-red-300">
<strong>駁回原因:</strong>{{ $member->disability_rejection_reason }}
</div>
@endif
@if($member->hasApprovedDisability() && $member->disability_verified_at)
<div class="mt-2 text-sm text-green-700 dark:text-green-300">
審核時間:{{ $member->disability_verified_at->format('Y/m/d H:i') }}
@if($member->disabilityVerifiedBy)
(審核人:{{ $member->disabilityVerifiedBy->name }}
@endif
</div>
@endif
</div>
<!-- Upload Form -->
@if(!$member->hasApprovedDisability())
<form method="post" action="{{ route('profile.disability-certificate.upload') }}" class="mt-6 space-y-6" enctype="multipart/form-data">
@csrf
<div>
<x-input-label for="disability_certificate" :value="__('上傳身心障礙手冊')" />
@if($member->hasDisabilityCertificate())
<div class="mt-2 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400">目前已上傳檔案,您可以重新上傳以替換。</p>
<a href="{{ route('profile.disability-certificate.view') }}" target="_blank" class="inline-flex items-center mt-2 text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
<svg class="w-4 h-4 mr-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
檢視已上傳的檔案
</a>
</div>
@endif
<input type="file" id="disability_certificate" name="disability_certificate"
class="mt-1 block w-full text-sm text-gray-500 dark:text-gray-400
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-indigo-50 file:text-indigo-700
dark:file:bg-indigo-900 dark:file:text-indigo-300
hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800"
accept=".jpg,.jpeg,.png,.pdf" required />
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
請上傳手冊封面及內頁照片(含照片、有效期)。支援 JPG、PNG、PDF 格式,最大 10MB。
</p>
<x-input-error class="mt-2" :messages="$errors->get('disability_certificate')" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('上傳') }}</x-primary-button>
@if (session('status') === 'disability-certificate-uploaded')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('已成功上傳') }}</p>
@endif
</div>
</form>
@else
<div class="mt-6">
<p class="text-sm text-gray-600 dark:text-gray-400">
您的身心障礙手冊已通過審核,繳費時將自動套用優惠。如需更新手冊,請聯繫管理員。
</p>
</div>
@endif
</section>

View File

@@ -1,10 +1,10 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Update Password') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Ensure your account is using a long, random password to stay secure.') }}
</p>
</header>
@@ -40,7 +40,7 @@
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
@endif
</div>

View File

@@ -1,10 +1,10 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Profile Information') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __("Update your account's profile information and email address.") }}
</p>
</header>
@@ -17,7 +17,7 @@
@if ($user->profilePhotoUrl())
<div class="flex items-center space-x-4">
<img src="{{ $user->profilePhotoUrl() }}" alt="{{ __('Profile photo for :name', ['name' => $user->name]) }}" class="h-16 w-16 rounded-full object-cover">
<p class="text-sm text-gray-600">{{ __('This is your current profile photo.') }}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ __('This is your current profile photo.') }}</p>
</div>
@endif
@@ -27,7 +27,7 @@
id="profile_photo"
name="profile_photo"
type="file"
class="mt-1 block w-full text-sm text-gray-900 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100"
class="mt-1 block w-full text-sm text-gray-900 dark:text-gray-100 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 dark:file:bg-indigo-900/50 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 dark:file:text-indigo-300 hover:file:bg-indigo-100 dark:hover:file:bg-indigo-900"
accept="image/*"
/>
<x-input-error class="mt-2" :messages="$errors->get('profile_photo')" />
@@ -49,16 +49,16 @@
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800">
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<p class="mt-2 font-medium text-sm text-green-600">
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
{{ __('A new verification link has been sent to your email address.') }}
</p>
@endif
@@ -162,7 +162,7 @@
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
@endif
</div>