Initial commit

This commit is contained in:
2025-11-20 23:21:05 +08:00
commit 13bc6db529
378 changed files with 54527 additions and 0 deletions

View File

@@ -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::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};

View File

@@ -0,0 +1,28 @@
<?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('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
}
};

View File

@@ -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::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -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::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@@ -0,0 +1,36 @@
<?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('document_categories', function (Blueprint $table) {
$table->id();
$table->string('name'); // 協會辦法, 法規, 會議記錄, 表格
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('icon')->nullable(); // emoji or FontAwesome class
$table->integer('sort_order')->default(0);
// Default access level for documents in this category
$table->enum('default_access_level', ['public', 'members', 'admin', 'board'])->default('members');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('document_categories');
}
};

View File

@@ -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('documents', function (Blueprint $table) {
$table->id();
$table->foreignId('document_category_id')->constrained()->onDelete('cascade');
// Document metadata
$table->string('title');
$table->string('document_number')->unique()->nullable(); // e.g., BYL-2024-001
$table->text('description')->nullable();
$table->uuid('public_uuid')->unique(); // For public sharing links
// Access control
$table->enum('access_level', ['public', 'members', 'admin', 'board'])->default('members');
// Current version pointer (set after first version is created)
$table->foreignId('current_version_id')->nullable()->constrained('document_versions')->onDelete('set null');
// Status
$table->enum('status', ['active', 'archived'])->default('active');
$table->timestamp('archived_at')->nullable();
// User tracking
$table->foreignId('created_by_user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('last_updated_by_user_id')->nullable()->constrained('users')->onDelete('set null');
// Statistics
$table->integer('view_count')->default(0);
$table->integer('download_count')->default(0);
$table->integer('version_count')->default(0);
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('document_category_id');
$table->index('access_level');
$table->index('status');
$table->index('public_uuid');
$table->index('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('documents');
}
};

View File

@@ -0,0 +1,55 @@
<?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('document_versions', function (Blueprint $table) {
$table->id();
$table->foreignId('document_id')->constrained()->onDelete('cascade');
// Version information
$table->string('version_number'); // 1.0, 1.1, 2.0, etc.
$table->text('version_notes')->nullable(); // What changed in this version
$table->boolean('is_current')->default(false); // Is this the current published version
// File information
$table->string('file_path'); // storage/documents/...
$table->string('original_filename');
$table->string('mime_type');
$table->unsignedBigInteger('file_size'); // in bytes
$table->string('file_hash')->nullable(); // SHA-256 hash for integrity verification
// User tracking
$table->foreignId('uploaded_by_user_id')->constrained('users')->onDelete('cascade');
$table->timestamp('uploaded_at');
// Make version immutable after creation (no updated_at)
$table->timestamps();
// Indexes
$table->index('document_id');
$table->index('version_number');
$table->index('is_current');
$table->index('uploaded_at');
// Unique constraint: only one current version per document
$table->unique(['document_id', 'is_current'], 'unique_current_version');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('document_versions');
}
};

View File

@@ -0,0 +1,44 @@
<?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('document_access_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('document_id')->constrained()->onDelete('cascade');
$table->foreignId('document_version_id')->nullable()->constrained()->onDelete('set null');
// Access information
$table->enum('action', ['view', 'download']); // What action was performed
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // null if anonymous/public access
$table->string('ip_address')->nullable();
$table->text('user_agent')->nullable();
// Timestamps
$table->timestamp('accessed_at');
$table->timestamps();
// Indexes
$table->index('document_id');
$table->index('user_id');
$table->index('action');
$table->index('accessed_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('document_access_logs');
}
};

View File

@@ -0,0 +1,35 @@
<?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('members', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('full_name');
$table->string('email')->index();
$table->string('phone')->nullable();
$table->string('national_id_encrypted')->nullable();
$table->string('national_id_hash')->nullable()->index();
$table->date('membership_started_at')->nullable();
$table->date('membership_expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('members');
}
};

View File

@@ -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::create('membership_payments', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->constrained()->cascadeOnDelete();
$table->date('paid_at');
$table->decimal('amount', 10, 2);
$table->string('method')->nullable();
$table->string('reference')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('membership_payments');
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false)->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};

View File

@@ -0,0 +1,134 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

View File

@@ -0,0 +1,27 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration {
public function up(): void
{
if (! class_exists(\Spatie\Permission\Models\Role::class)) {
return;
}
$roleClass = \Spatie\Permission\Models\Role::class;
$adminRole = $roleClass::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
User::where('is_admin', true)->each(function (User $user) use ($adminRole) {
$user->assignRole($adminRole);
});
}
public function down(): void
{
// no-op
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('members', function (Blueprint $table) {
$table->timestamp('last_expiry_reminder_sent_at')->nullable()->after('membership_expires_at');
});
}
public function down(): void
{
Schema::table('members', function (Blueprint $table) {
$table->dropColumn('last_expiry_reminder_sent_at');
});
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action');
$table->string('auditable_type')->nullable();
$table->unsignedBigInteger('auditable_id')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('audit_logs');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('finance_documents', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('submitted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('title');
$table->decimal('amount', 10, 2)->nullable();
$table->string('status')->default('pending');
$table->text('description')->nullable();
$table->timestamp('submitted_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('finance_documents');
}
};

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('members', function (Blueprint $table) {
$table->string('address_line_1')->nullable()->after('phone');
$table->string('address_line_2')->nullable()->after('address_line_1');
$table->string('city')->nullable()->after('address_line_2');
$table->string('postal_code')->nullable()->after('city');
});
}
public function down(): void
{
Schema::table('members', function (Blueprint $table) {
$table->dropColumn(['address_line_1', 'address_line_2', 'city', 'postal_code']);
});
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('roles', function (Blueprint $table) {
$table->string('description')->nullable()->after('guard_name');
});
}
public function down(): void
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('description');
});
}
};

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('members', function (Blueprint $table) {
$table->string('emergency_contact_name')->nullable()->after('postal_code');
$table->string('emergency_contact_phone')->nullable()->after('emergency_contact_name');
});
}
public function down(): void
{
Schema::table('members', function (Blueprint $table) {
$table->dropColumn(['emergency_contact_name', 'emergency_contact_phone']);
});
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('profile_photo_path')->nullable()->after('is_admin');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('profile_photo_path');
});
}
};

View File

@@ -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::table('finance_documents', function (Blueprint $table) {
// File attachment
$table->string('attachment_path')->nullable()->after('description');
// Cashier approval
$table->foreignId('approved_by_cashier_id')->nullable()->constrained('users')->nullOnDelete()->after('attachment_path');
$table->timestamp('cashier_approved_at')->nullable()->after('approved_by_cashier_id');
// Accountant approval
$table->foreignId('approved_by_accountant_id')->nullable()->constrained('users')->nullOnDelete()->after('cashier_approved_at');
$table->timestamp('accountant_approved_at')->nullable()->after('approved_by_accountant_id');
// Chair approval
$table->foreignId('approved_by_chair_id')->nullable()->constrained('users')->nullOnDelete()->after('accountant_approved_at');
$table->timestamp('chair_approved_at')->nullable()->after('approved_by_chair_id');
// Rejection fields
$table->foreignId('rejected_by_user_id')->nullable()->constrained('users')->nullOnDelete()->after('chair_approved_at');
$table->timestamp('rejected_at')->nullable()->after('rejected_by_user_id');
$table->text('rejection_reason')->nullable()->after('rejected_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('finance_documents', function (Blueprint $table) {
$table->dropForeign(['approved_by_cashier_id']);
$table->dropForeign(['approved_by_accountant_id']);
$table->dropForeign(['approved_by_chair_id']);
$table->dropForeign(['rejected_by_user_id']);
$table->dropColumn([
'attachment_path',
'approved_by_cashier_id',
'cashier_approved_at',
'approved_by_accountant_id',
'accountant_approved_at',
'approved_by_chair_id',
'chair_approved_at',
'rejected_by_user_id',
'rejected_at',
'rejection_reason',
]);
});
}
};

View File

@@ -0,0 +1,39 @@
<?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('chart_of_accounts', function (Blueprint $table) {
$table->id();
$table->string('account_code', 10)->unique()->comment('Account code (e.g., 4101)');
$table->string('account_name_zh')->comment('Chinese account name');
$table->string('account_name_en')->nullable()->comment('English account name');
$table->enum('account_type', ['asset', 'liability', 'net_asset', 'income', 'expense'])->comment('Account type');
$table->string('category')->nullable()->comment('Detailed category');
$table->foreignId('parent_account_id')->nullable()->constrained('chart_of_accounts')->nullOnDelete()->comment('Parent account for hierarchical structure');
$table->boolean('is_active')->default(true)->comment('Active status');
$table->integer('display_order')->default(0)->comment('Display order');
$table->text('description')->nullable()->comment('Account description');
$table->timestamps();
$table->index('account_type');
$table->index('is_active');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chart_of_accounts');
}
};

View File

@@ -0,0 +1,34 @@
<?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('budget_items', function (Blueprint $table) {
$table->id();
$table->foreignId('budget_id')->constrained()->cascadeOnDelete()->comment('Budget reference');
$table->foreignId('chart_of_account_id')->constrained()->cascadeOnDelete()->comment('Chart of account reference');
$table->decimal('budgeted_amount', 15, 2)->default(0)->comment('Budgeted amount');
$table->decimal('actual_amount', 15, 2)->default(0)->comment('Actual amount (calculated)');
$table->text('notes')->nullable()->comment('Item notes');
$table->timestamps();
$table->index(['budget_id', 'chart_of_account_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('budget_items');
}
};

View File

@@ -0,0 +1,40 @@
<?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('budgets', function (Blueprint $table) {
$table->id();
$table->integer('fiscal_year')->comment('Fiscal year (e.g., 2025)');
$table->string('name')->comment('Budget name');
$table->enum('period_type', ['annual', 'quarterly', 'monthly'])->default('annual')->comment('Budget period type');
$table->date('period_start')->comment('Period start date');
$table->date('period_end')->comment('Period end date');
$table->enum('status', ['draft', 'submitted', 'approved', 'active', 'closed'])->default('draft')->comment('Budget status');
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete()->comment('Created by user');
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('Approved by user');
$table->timestamp('approved_at')->nullable()->comment('Approval timestamp');
$table->text('notes')->nullable()->comment('Budget notes');
$table->timestamps();
$table->index('fiscal_year');
$table->index('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('budgets');
}
};

View File

@@ -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::create('transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('budget_item_id')->nullable()->constrained()->nullOnDelete()->comment('Budget item reference');
$table->foreignId('chart_of_account_id')->constrained()->cascadeOnDelete()->comment('Chart of account reference');
$table->date('transaction_date')->comment('Transaction date');
$table->decimal('amount', 15, 2)->comment('Transaction amount');
$table->enum('transaction_type', ['income', 'expense'])->comment('Transaction type');
$table->string('description')->comment('Transaction description');
$table->string('reference_number')->nullable()->comment('Reference/receipt number');
$table->foreignId('finance_document_id')->nullable()->constrained()->nullOnDelete()->comment('Related finance document');
$table->foreignId('membership_payment_id')->nullable()->constrained()->nullOnDelete()->comment('Related membership payment');
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete()->comment('Created by user');
$table->text('notes')->nullable()->comment('Additional notes');
$table->timestamps();
$table->index('transaction_date');
$table->index('transaction_type');
$table->index(['budget_item_id', 'transaction_date']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('transactions');
}
};

View File

@@ -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::create('financial_reports', function (Blueprint $table) {
$table->id();
$table->enum('report_type', ['revenue_expenditure', 'balance_sheet', 'property_inventory', 'internal_management'])->comment('Report type');
$table->integer('fiscal_year')->comment('Fiscal year');
$table->date('period_start')->comment('Period start date');
$table->date('period_end')->comment('Period end date');
$table->enum('status', ['draft', 'finalized', 'approved', 'submitted'])->default('draft')->comment('Report status');
$table->foreignId('budget_id')->nullable()->constrained()->nullOnDelete()->comment('Related budget');
$table->foreignId('generated_by_user_id')->constrained('users')->cascadeOnDelete()->comment('Generated by user');
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('Approved by user');
$table->timestamp('approved_at')->nullable()->comment('Approval timestamp');
$table->string('file_path')->nullable()->comment('PDF/Excel file path');
$table->text('notes')->nullable()->comment('Report notes');
$table->timestamps();
$table->index(['report_type', 'fiscal_year']);
$table->index('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('financial_reports');
}
};

View File

@@ -0,0 +1,66 @@
<?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('issues', function (Blueprint $table) {
$table->id();
$table->string('issue_number')->unique()->comment('Auto-generated issue number (e.g., ISS-2025-001)');
$table->string('title');
$table->text('description')->nullable();
// Issue categorization
$table->enum('issue_type', ['work_item', 'project_task', 'maintenance', 'member_request'])
->default('work_item')
->comment('Type of issue');
$table->enum('status', ['new', 'assigned', 'in_progress', 'review', 'closed'])
->default('new')
->comment('Current workflow status');
$table->enum('priority', ['low', 'medium', 'high', 'urgent'])
->default('medium')
->comment('Priority level');
// User relationships
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete()->comment('User who created the issue');
$table->foreignId('assigned_to_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('User assigned to work on this');
$table->foreignId('reviewer_id')->nullable()->constrained('users')->nullOnDelete()->comment('User assigned to review');
// Related entities
$table->foreignId('member_id')->nullable()->constrained('members')->nullOnDelete()->comment('Related member (for member requests)');
$table->foreignId('parent_issue_id')->nullable()->constrained('issues')->nullOnDelete()->comment('Parent issue for sub-tasks');
// Dates and time tracking
$table->date('due_date')->nullable()->comment('Deadline for completion');
$table->timestamp('closed_at')->nullable()->comment('When issue was closed');
$table->decimal('estimated_hours', 8, 2)->nullable()->comment('Estimated time to complete');
$table->decimal('actual_hours', 8, 2)->default(0)->comment('Actual time spent (sum of time logs)');
$table->timestamps();
$table->softDeletes();
// Indexes for common queries
$table->index('issue_type');
$table->index('status');
$table->index('priority');
$table->index('assigned_to_user_id');
$table->index('created_by_user_id');
$table->index('due_date');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issues');
}
};

View File

@@ -0,0 +1,35 @@
<?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('issue_comments', function (Blueprint $table) {
$table->id();
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->text('comment_text');
$table->boolean('is_internal')->default(false)->comment('Hide from members if true');
$table->timestamps();
// Indexes
$table->index('issue_id');
$table->index('user_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_comments');
}
};

View File

@@ -0,0 +1,36 @@
<?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('issue_attachments', function (Blueprint $table) {
$table->id();
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete()->comment('User who uploaded');
$table->string('file_name');
$table->string('file_path');
$table->unsignedBigInteger('file_size')->comment('File size in bytes');
$table->string('mime_type');
$table->timestamps();
// Indexes
$table->index('issue_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_attachments');
}
};

View File

@@ -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::create('custom_field_values', function (Blueprint $table) {
$table->id();
$table->foreignId('custom_field_id')->constrained('custom_fields')->cascadeOnDelete();
$table->morphs('customizable'); // customizable_type and customizable_id (for issues)
$table->json('value')->comment('Stored value (JSON for flexibility)');
$table->timestamps();
// Indexes
$table->index('custom_field_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('custom_field_values');
}
};

View File

@@ -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::create('custom_fields', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->enum('field_type', ['text', 'number', 'date', 'select'])->comment('Data type');
$table->json('options')->nullable()->comment('Options for select type fields');
$table->json('applies_to_issue_types')->comment('Which issue types can use this field');
$table->boolean('is_required')->default(false);
$table->integer('display_order')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('custom_fields');
}
};

View File

@@ -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('issue_label_pivot', function (Blueprint $table) {
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
$table->foreignId('issue_label_id')->constrained('issue_labels')->cascadeOnDelete();
$table->timestamps();
// Composite primary key
$table->primary(['issue_id', 'issue_label_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_label_pivot');
}
};

View File

@@ -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.
*/
public function up(): void
{
Schema::create('issue_labels', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('color', 7)->default('#6B7280')->comment('Hex color code');
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_labels');
}
};

View File

@@ -0,0 +1,35 @@
<?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('issue_relationships', function (Blueprint $table) {
$table->id();
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
$table->foreignId('related_issue_id')->constrained('issues')->cascadeOnDelete();
$table->enum('relationship_type', ['blocks', 'blocked_by', 'related_to', 'duplicate_of'])
->comment('Type of relationship');
$table->timestamps();
// Indexes
$table->index('issue_id');
$table->index('related_issue_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_relationships');
}
};

View File

@@ -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('issue_time_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->decimal('hours', 8, 2)->comment('Hours worked');
$table->text('description')->nullable()->comment('What was done');
$table->timestamp('logged_at')->comment('When the work was performed');
$table->timestamps();
// Indexes
$table->index('issue_id');
$table->index('user_id');
$table->index('logged_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_time_logs');
}
};

View File

@@ -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('issue_watchers', function (Blueprint $table) {
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamps();
// Composite primary key to prevent duplicate watchers
$table->primary(['issue_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('issue_watchers');
}
};

View File

@@ -0,0 +1,82 @@
<?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) {
// Payment verification workflow status
$table->enum('status', ['pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected'])
->default('pending')
->after('reference');
// Payment method
$table->enum('payment_method', ['bank_transfer', 'convenience_store', 'cash', 'credit_card'])
->nullable()
->after('status');
// Receipt file upload
$table->string('receipt_path')->nullable()->after('payment_method');
// Submitted by (member self-submission)
$table->foreignId('submitted_by_user_id')->nullable()->after('receipt_path')
->constrained('users')->nullOnDelete();
// Cashier verification (Tier 1)
$table->foreignId('verified_by_cashier_id')->nullable()->after('submitted_by_user_id')
->constrained('users')->nullOnDelete();
$table->timestamp('cashier_verified_at')->nullable()->after('verified_by_cashier_id');
// Accountant verification (Tier 2)
$table->foreignId('verified_by_accountant_id')->nullable()->after('cashier_verified_at')
->constrained('users')->nullOnDelete();
$table->timestamp('accountant_verified_at')->nullable()->after('verified_by_accountant_id');
// Chair verification (Tier 3)
$table->foreignId('verified_by_chair_id')->nullable()->after('accountant_verified_at')
->constrained('users')->nullOnDelete();
$table->timestamp('chair_verified_at')->nullable()->after('verified_by_chair_id');
// Rejection tracking
$table->foreignId('rejected_by_user_id')->nullable()->after('chair_verified_at')
->constrained('users')->nullOnDelete();
$table->timestamp('rejected_at')->nullable()->after('rejected_by_user_id');
$table->text('rejection_reason')->nullable()->after('rejected_at');
// Admin notes
$table->text('notes')->nullable()->after('rejection_reason');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('membership_payments', function (Blueprint $table) {
$table->dropColumn([
'status',
'payment_method',
'receipt_path',
'submitted_by_user_id',
'verified_by_cashier_id',
'cashier_verified_at',
'verified_by_accountant_id',
'accountant_verified_at',
'verified_by_chair_id',
'chair_verified_at',
'rejected_by_user_id',
'rejected_at',
'rejection_reason',
'notes',
]);
});
}
};

View File

@@ -0,0 +1,38 @@
<?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) {
// Membership status - distinguishes paid vs unpaid members
$table->enum('membership_status', ['pending', 'active', 'expired', 'suspended'])
->default('pending')
->after('membership_expires_at')
->comment('Payment verification status: pending (not paid), active (paid & activated), expired, suspended');
// Membership type - for different membership tiers
$table->enum('membership_type', ['regular', 'honorary', 'lifetime', 'student'])
->default('regular')
->after('membership_status')
->comment('Type of membership: regular (annual fee), honorary (no fee), lifetime (one-time), student (discounted)');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('members', function (Blueprint $table) {
$table->dropColumn(['membership_status', 'membership_type']);
});
}
};

View File

@@ -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.
*
* The unique constraint on (document_id, is_current) was too restrictive.
* It prevented having multiple versions with is_current = false.
* We only need to ensure ONE version has is_current = true, which is
* enforced in the application logic.
*/
public function up(): void
{
Schema::table('document_versions', function (Blueprint $table) {
$table->dropUnique('unique_current_version');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('document_versions', function (Blueprint $table) {
$table->unique(['document_id', 'is_current'], 'unique_current_version');
});
}
};

View File

@@ -0,0 +1,43 @@
<?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
{
// Create tags table
Schema::create('document_tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('color')->default('#6366f1'); // Indigo color
$table->text('description')->nullable();
$table->timestamps();
});
// Create pivot table for document-tag relationship
Schema::create('document_document_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('document_id')->constrained()->onDelete('cascade');
$table->foreignId('document_tag_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['document_id', 'document_tag_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('document_document_tag');
Schema::dropIfExists('document_tags');
}
};

View File

@@ -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.
*/
public function up(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->date('expires_at')->nullable()->after('status');
$table->boolean('auto_archive_on_expiry')->default(false)->after('expires_at');
$table->text('expiry_notice')->nullable()->after('auto_archive_on_expiry');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn(['expires_at', 'auto_archive_on_expiry', 'expiry_notice']);
});
}
};

View File

@@ -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::create('system_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique()->comment('Setting key (e.g., documents.qr_enabled)');
$table->text('value')->nullable()->comment('Setting value (JSON for complex values)');
$table->enum('type', ['string', 'integer', 'boolean', 'json', 'array'])->default('string')->comment('Value type for casting');
$table->string('group')->nullable()->index()->comment('Settings group (e.g., documents, security, notifications)');
$table->text('description')->nullable()->comment('Human-readable description of this setting');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('system_settings');
}
};

View File

@@ -0,0 +1,124 @@
<?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) {
// 申請類型和金額分級
$table->enum('request_type', ['expense_reimbursement', 'advance_payment', 'purchase_request', 'petty_cash'])
->default('expense_reimbursement')
->after('status')
->comment('申請類型:費用報銷/預支借款/採購申請/零用金');
$table->enum('amount_tier', ['small', 'medium', 'large'])
->nullable()
->after('request_type')
->comment('金額層級:小額(<5000)/中額(5000-50000)/大額(>50000)');
// 會計科目分配(會計審核時填寫)
$table->foreignId('chart_of_account_id')->nullable()->after('amount_tier')->constrained('chart_of_accounts')->nullOnDelete();
$table->foreignId('budget_item_id')->nullable()->after('chart_of_account_id')->constrained('budget_items')->nullOnDelete();
// 理監事會議核准(大額)
$table->boolean('requires_board_meeting')->default(false)->after('chair_approved_at');
$table->date('board_meeting_date')->nullable()->after('requires_board_meeting');
$table->text('board_meeting_decision')->nullable()->after('board_meeting_date');
$table->foreignId('approved_by_board_meeting_id')->nullable()->after('board_meeting_decision')->constrained('users')->nullOnDelete();
$table->timestamp('board_meeting_approved_at')->nullable()->after('approved_by_board_meeting_id');
// 付款單製作(會計)
$table->foreignId('payment_order_created_by_accountant_id')->nullable()->after('board_meeting_approved_at')->constrained('users')->nullOnDelete();
$table->timestamp('payment_order_created_at')->nullable()->after('payment_order_created_by_accountant_id');
$table->enum('payment_method', ['bank_transfer', 'check', 'cash'])->nullable()->after('payment_order_created_at');
$table->string('payee_name', 100)->nullable()->after('payment_method');
$table->string('payee_bank_code', 10)->nullable()->after('payee_name');
$table->string('payee_account_number', 30)->nullable()->after('payee_bank_code');
$table->text('payment_notes')->nullable()->after('payee_account_number');
// 出納覆核付款單
$table->foreignId('payment_verified_by_cashier_id')->nullable()->after('payment_notes')->constrained('users')->nullOnDelete();
$table->timestamp('payment_verified_at')->nullable()->after('payment_verified_by_cashier_id');
$table->text('payment_verification_notes')->nullable()->after('payment_verified_at');
// 實際付款執行
$table->foreignId('payment_executed_by_cashier_id')->nullable()->after('payment_verification_notes')->constrained('users')->nullOnDelete();
$table->timestamp('payment_executed_at')->nullable()->after('payment_executed_by_cashier_id');
$table->string('payment_transaction_id', 50)->nullable()->after('payment_executed_at')->comment('銀行交易編號');
$table->string('payment_receipt_path')->nullable()->after('payment_transaction_id')->comment('付款憑證路徑');
$table->decimal('actual_payment_amount', 10, 2)->nullable()->after('payment_receipt_path')->comment('實付金額');
// 記帳階段 (外鍵稍後加上,因為相關表還不存在)
$table->unsignedBigInteger('cashier_ledger_entry_id')->nullable()->after('actual_payment_amount');
$table->timestamp('cashier_recorded_at')->nullable()->after('cashier_ledger_entry_id');
$table->unsignedBigInteger('accounting_transaction_id')->nullable()->after('cashier_recorded_at');
$table->timestamp('accountant_recorded_at')->nullable()->after('accounting_transaction_id');
// 月底核對
$table->enum('reconciliation_status', ['pending', 'matched', 'discrepancy', 'resolved'])->default('pending')->after('accountant_recorded_at');
$table->text('reconciliation_notes')->nullable()->after('reconciliation_status');
$table->timestamp('reconciled_at')->nullable()->after('reconciliation_notes');
$table->foreignId('reconciled_by_user_id')->nullable()->after('reconciled_at')->constrained('users')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('finance_documents', function (Blueprint $table) {
// 移除外鍵約束
$table->dropForeign(['chart_of_account_id']);
$table->dropForeign(['budget_item_id']);
$table->dropForeign(['approved_by_board_meeting_id']);
$table->dropForeign(['payment_order_created_by_accountant_id']);
$table->dropForeign(['payment_verified_by_cashier_id']);
$table->dropForeign(['payment_executed_by_cashier_id']);
$table->dropForeign(['reconciled_by_user_id']);
// 移除欄位
$table->dropColumn([
'request_type',
'amount_tier',
'chart_of_account_id',
'budget_item_id',
'requires_board_meeting',
'board_meeting_date',
'board_meeting_decision',
'approved_by_board_meeting_id',
'board_meeting_approved_at',
'payment_order_created_by_accountant_id',
'payment_order_created_at',
'payment_method',
'payee_name',
'payee_bank_code',
'payee_account_number',
'payment_notes',
'payment_verified_by_cashier_id',
'payment_verified_at',
'payment_verification_notes',
'payment_executed_by_cashier_id',
'payment_executed_at',
'payment_transaction_id',
'payment_receipt_path',
'actual_payment_amount',
'cashier_ledger_entry_id',
'cashier_recorded_at',
'accounting_transaction_id',
'accountant_recorded_at',
'reconciliation_status',
'reconciliation_notes',
'reconciled_at',
'reconciled_by_user_id',
]);
});
}
};

View File

@@ -0,0 +1,64 @@
<?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('payment_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('finance_document_id')->constrained('finance_documents')->cascadeOnDelete();
// 付款資訊
$table->string('payee_name', 100)->comment('收款人姓名');
$table->string('payee_bank_code', 10)->nullable()->comment('銀行代碼');
$table->string('payee_account_number', 30)->nullable()->comment('銀行帳號');
$table->string('payee_bank_name', 100)->nullable()->comment('銀行名稱');
$table->decimal('payment_amount', 10, 2)->comment('付款金額');
$table->enum('payment_method', ['bank_transfer', 'check', 'cash'])->comment('付款方式');
// 會計製單
$table->foreignId('created_by_accountant_id')->constrained('users')->cascadeOnDelete();
$table->string('payment_order_number', 50)->unique()->comment('付款單號');
$table->text('notes')->nullable();
// 出納覆核
$table->foreignId('verified_by_cashier_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('verified_at')->nullable();
$table->enum('verification_status', ['pending', 'approved', 'rejected'])->default('pending');
$table->text('verification_notes')->nullable();
// 執行付款
$table->foreignId('executed_by_cashier_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('executed_at')->nullable();
$table->enum('execution_status', ['pending', 'completed', 'failed'])->default('pending');
$table->string('transaction_reference', 100)->nullable()->comment('交易參考號');
// 憑證
$table->string('payment_receipt_path')->nullable()->comment('付款憑證路徑');
$table->enum('status', ['draft', 'pending_verification', 'verified', 'executed', 'cancelled'])->default('draft');
$table->timestamps();
$table->index('finance_document_id');
$table->index('status');
$table->index('verification_status');
$table->index('execution_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('payment_orders');
}
};

View File

@@ -0,0 +1,54 @@
<?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('cashier_ledger_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('finance_document_id')->nullable()->constrained('finance_documents')->cascadeOnDelete();
$table->date('entry_date')->comment('記帳日期');
$table->enum('entry_type', ['receipt', 'payment'])->comment('類型:收入/支出');
// 付款資訊
$table->enum('payment_method', ['bank_transfer', 'check', 'cash'])->comment('付款方式');
$table->string('bank_account', 100)->nullable()->comment('使用的銀行帳戶');
$table->decimal('amount', 10, 2)->comment('金額');
// 餘額追蹤
$table->decimal('balance_before', 10, 2)->comment('交易前餘額');
$table->decimal('balance_after', 10, 2)->comment('交易後餘額');
// 憑證資訊
$table->string('receipt_number', 50)->nullable()->comment('收據/憑證編號');
$table->string('transaction_reference', 100)->nullable()->comment('交易參考號');
// 記錄人員
$table->foreignId('recorded_by_cashier_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('recorded_at')->useCurrent();
$table->text('notes')->nullable();
$table->timestamps();
$table->index('finance_document_id');
$table->index('entry_date');
$table->index('entry_type');
$table->index('recorded_by_cashier_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cashier_ledger_entries');
}
};

View File

@@ -0,0 +1,60 @@
<?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('bank_reconciliations', function (Blueprint $table) {
$table->id();
$table->date('reconciliation_month')->comment('調節月份');
// 銀行對帳單
$table->decimal('bank_statement_balance', 10, 2)->comment('銀行對帳單餘額');
$table->date('bank_statement_date')->comment('對帳單日期');
$table->string('bank_statement_file_path')->nullable()->comment('對帳單檔案');
// 系統帳面
$table->decimal('system_book_balance', 10, 2)->comment('系統帳面餘額');
// 未達帳項JSON 格式)
$table->json('outstanding_checks')->nullable()->comment('未兌現支票');
$table->json('deposits_in_transit')->nullable()->comment('在途存款');
$table->json('bank_charges')->nullable()->comment('銀行手續費');
// 調節結果
$table->decimal('adjusted_balance', 10, 2)->comment('調整後餘額');
$table->decimal('discrepancy_amount', 10, 2)->default(0)->comment('差異金額');
$table->enum('reconciliation_status', ['pending', 'completed', 'discrepancy'])->default('pending');
// 執行人員
$table->foreignId('prepared_by_cashier_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('reviewed_by_accountant_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('approved_by_manager_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('prepared_at')->useCurrent();
$table->timestamp('reviewed_at')->nullable();
$table->timestamp('approved_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->index('reconciliation_month');
$table->index('reconciliation_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bank_reconciliations');
}
};