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:
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* 移除 is_admin 欄位,統一使用 Spatie Permission 的 admin 角色進行權限管理
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('email');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('cashier_ledger_entries')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('cashier_ledger_entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('finance_document_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->date('entry_date');
|
||||
$table->string('entry_type'); // receipt, payment
|
||||
$table->string('payment_method'); // bank_transfer, check, cash
|
||||
$table->string('bank_account')->nullable();
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->decimal('balance_before', 12, 2)->default(0);
|
||||
$table->decimal('balance_after', 12, 2)->default(0);
|
||||
$table->string('receipt_number')->nullable();
|
||||
$table->string('transaction_reference')->nullable();
|
||||
$table->foreignId('recorded_by_cashier_id')->constrained('users');
|
||||
$table->timestamp('recorded_at');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('entry_date');
|
||||
$table->index('entry_type');
|
||||
$table->index('bank_account');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cashier_ledger_entries');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('announcements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// 基本資訊
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
|
||||
// 狀態管理
|
||||
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
|
||||
|
||||
// 顯示控制
|
||||
$table->boolean('is_pinned')->default(false);
|
||||
$table->integer('display_order')->default(0);
|
||||
|
||||
// 訪問控制
|
||||
$table->enum('access_level', ['public', 'members', 'board', 'admin'])->default('members');
|
||||
|
||||
// 時間控制
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('archived_at')->nullable();
|
||||
|
||||
// 統計
|
||||
$table->integer('view_count')->default(0);
|
||||
|
||||
// 用戶關聯
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('last_updated_by_user_id')->nullable()->constrained('users')->onDelete('set null');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 索引
|
||||
$table->index('status');
|
||||
$table->index('access_level');
|
||||
$table->index('published_at');
|
||||
$table->index('expires_at');
|
||||
$table->index(['is_pinned', 'display_order']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('announcements');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('accounting_entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('finance_document_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('chart_of_account_id')->constrained();
|
||||
$table->enum('entry_type', ['debit', 'credit']);
|
||||
$table->decimal('amount', 15, 2);
|
||||
$table->date('entry_date');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['finance_document_id', 'entry_type']);
|
||||
$table->index(['chart_of_account_id', 'entry_date']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('accounting_entries');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('board_meetings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('meeting_date');
|
||||
$table->string('title');
|
||||
$table->text('notes')->nullable();
|
||||
$table->enum('status', ['scheduled', 'completed', 'cancelled'])->default('scheduled');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('board_meetings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
// 只新增尚未存在的欄位
|
||||
if (!Schema::hasColumn('finance_documents', 'accountant_recorded_by_id')) {
|
||||
$table->unsignedBigInteger('accountant_recorded_by_id')->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('finance_documents', 'accountant_recorded_by_id')) {
|
||||
$table->dropColumn('accountant_recorded_by_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('finance_documents', 'request_type')) {
|
||||
$table->dropColumn('request_type');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('finance_documents', 'request_type')) {
|
||||
$table->string('request_type')->nullable()->after('amount');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('incomes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// 基本資訊
|
||||
$table->string('income_number')->unique(); // 收入編號:INC-2025-0001
|
||||
$table->string('title'); // 收入標題
|
||||
$table->text('description')->nullable(); // 說明
|
||||
$table->date('income_date'); // 收入日期
|
||||
$table->decimal('amount', 12, 2); // 金額
|
||||
|
||||
// 收入分類
|
||||
$table->string('income_type'); // 收入類型
|
||||
$table->foreignId('chart_of_account_id') // 會計科目
|
||||
->constrained('chart_of_accounts');
|
||||
|
||||
// 付款資訊
|
||||
$table->string('payment_method'); // 付款方式
|
||||
$table->string('bank_account')->nullable(); // 銀行帳戶
|
||||
$table->string('payer_name')->nullable(); // 付款人姓名
|
||||
$table->string('receipt_number')->nullable(); // 收據編號
|
||||
$table->string('transaction_reference')->nullable(); // 銀行交易參考號
|
||||
$table->string('attachment_path')->nullable(); // 附件路徑
|
||||
|
||||
// 會員關聯
|
||||
$table->foreignId('member_id')->nullable()
|
||||
->constrained()->nullOnDelete();
|
||||
|
||||
// 審核流程
|
||||
$table->string('status')->default('pending'); // pending, confirmed, cancelled
|
||||
|
||||
// 出納記錄
|
||||
$table->foreignId('recorded_by_cashier_id')
|
||||
->constrained('users');
|
||||
$table->timestamp('recorded_at');
|
||||
|
||||
// 會計確認
|
||||
$table->foreignId('confirmed_by_accountant_id')->nullable()
|
||||
->constrained('users');
|
||||
$table->timestamp('confirmed_at')->nullable();
|
||||
|
||||
// 關聯出納日記帳
|
||||
$table->foreignId('cashier_ledger_entry_id')->nullable()
|
||||
->constrained('cashier_ledger_entries')->nullOnDelete();
|
||||
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// 索引
|
||||
$table->index('income_date');
|
||||
$table->index('income_type');
|
||||
$table->index('status');
|
||||
$table->index(['member_id', 'income_type']);
|
||||
});
|
||||
|
||||
// 在 accounting_entries 表新增 income_id 欄位
|
||||
Schema::table('accounting_entries', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('accounting_entries', 'income_id')) {
|
||||
$table->foreignId('income_id')->nullable()
|
||||
->after('finance_document_id')
|
||||
->constrained('incomes')->nullOnDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('accounting_entries', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('accounting_entries', 'income_id')) {
|
||||
$table->dropForeign(['income_id']);
|
||||
$table->dropColumn('income_id');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::dropIfExists('incomes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
// 身心障礙手冊相關欄位
|
||||
$table->string('disability_certificate_path')->nullable()->after('membership_type');
|
||||
$table->string('disability_certificate_status')->nullable()->after('disability_certificate_path');
|
||||
$table->foreignId('disability_verified_by')->nullable()->after('disability_certificate_status')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('disability_verified_at')->nullable()->after('disability_verified_by');
|
||||
$table->text('disability_rejection_reason')->nullable()->after('disability_verified_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropForeign(['disability_verified_by']);
|
||||
$table->dropColumn([
|
||||
'disability_certificate_path',
|
||||
'disability_certificate_status',
|
||||
'disability_verified_by',
|
||||
'disability_verified_at',
|
||||
'disability_rejection_reason',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('membership_payments', function (Blueprint $table) {
|
||||
// 會費類型欄位
|
||||
$table->string('fee_type')->default('entrance_fee')->after('member_id');
|
||||
$table->decimal('base_amount', 10, 2)->nullable()->after('amount');
|
||||
$table->decimal('discount_amount', 10, 2)->default(0)->after('base_amount');
|
||||
$table->decimal('final_amount', 10, 2)->nullable()->after('discount_amount');
|
||||
$table->boolean('disability_discount')->default(false)->after('final_amount');
|
||||
});
|
||||
|
||||
// 為現有記錄設定預設值
|
||||
\DB::statement("UPDATE membership_payments SET base_amount = amount, final_amount = amount WHERE base_amount IS NULL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('membership_payments', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'fee_type',
|
||||
'base_amount',
|
||||
'discount_amount',
|
||||
'final_amount',
|
||||
'disability_discount',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user