Implement dark mode, bug report page, and schema dump

This commit is contained in:
2025-11-27 15:06:45 +08:00
parent 13bc6db529
commit 83602b1ed1
91 changed files with 1078 additions and 2291 deletions

View File

@@ -0,0 +1,43 @@
<?php
namespace Database\Factories;
use App\Models\Budget;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class BudgetFactory extends Factory
{
protected $model = Budget::class;
public function definition(): array
{
$start = now()->startOfYear();
return [
'fiscal_year' => now()->year,
'name' => $this->faker->sentence(3),
'period_type' => 'annual',
'period_start' => $start,
'period_end' => $start->copy()->endOfYear(),
'status' => Budget::STATUS_DRAFT,
'created_by_user_id' => User::factory(),
'approved_by_user_id' => null,
'approved_at' => null,
'notes' => $this->faker->sentence(),
];
}
public function submitted(): static
{
return $this->state(fn () => ['status' => Budget::STATUS_SUBMITTED]);
}
public function approved(): static
{
return $this->state(fn () => [
'status' => Budget::STATUS_APPROVED,
'approved_at' => now(),
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Database\Factories;
use App\Models\Budget;
use App\Models\BudgetItem;
use App\Models\ChartOfAccount;
use Illuminate\Database\Eloquent\Factories\Factory;
class BudgetItemFactory extends Factory
{
protected $model = BudgetItem::class;
public function definition(): array
{
$account = ChartOfAccount::first();
if (! $account) {
$account = ChartOfAccount::create([
'account_code' => $this->faker->unique()->numerify('4###'),
'account_name_zh' => '測試費用',
'account_name_en' => 'Test Expense',
'account_type' => 'expense',
'category' => 'operating',
'is_active' => true,
'display_order' => 1,
]);
}
return [
'budget_id' => Budget::factory(),
'chart_of_account_id' => $account->id,
'budgeted_amount' => $this->faker->numberBetween(1000, 5000),
'actual_amount' => $this->faker->numberBetween(0, 4000),
'notes' => $this->faker->sentence(),
];
}
}

View File

@@ -20,23 +20,34 @@ class FinanceDocumentFactory extends Factory
*/
public function definition(): array
{
$amount = $this->faker->randomFloat(2, 100, 100000);
$requestTypes = ['expense_reimbursement', 'advance_payment', 'purchase_request', 'petty_cash'];
$statuses = ['pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected'];
return [
'title' => $this->faker->sentence(6),
'description' => $this->faker->paragraph(3),
'amount' => $amount,
'amount' => $this->faker->randomFloat(2, 100, 100000),
'request_type' => $this->faker->randomElement($requestTypes),
'status' => $this->faker->randomElement($statuses),
'submitted_by_id' => User::factory(),
'submitted_by_user_id' => User::factory(),
'submitted_at' => now(),
'amount_tier' => $this->determineAmountTier($amount),
'requires_board_meeting' => $amount > 50000,
'amount_tier' => null,
'requires_board_meeting' => false,
];
}
public function configure()
{
return $this->afterMaking(function (FinanceDocument $document) {
$document->amount_tier = $document->determineAmountTier();
$document->requires_board_meeting = $document->needsBoardMeetingApproval();
})->afterCreating(function (FinanceDocument $document) {
$document->amount_tier = $document->determineAmountTier();
$document->requires_board_meeting = $document->needsBoardMeetingApproval();
$document->save();
});
}
/**
* Indicate that the document is pending approval.
*/
@@ -54,7 +65,7 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
'cashier_approved_by_id' => User::factory(),
'approved_by_cashier_id' => User::factory(),
'cashier_approved_at' => now(),
]);
}
@@ -66,9 +77,9 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
'cashier_approved_by_id' => User::factory(),
'approved_by_cashier_id' => User::factory(),
'cashier_approved_at' => now(),
'accountant_approved_by_id' => User::factory(),
'approved_by_accountant_id' => User::factory(),
'accountant_approved_at' => now(),
]);
}
@@ -80,11 +91,11 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
'cashier_approved_by_id' => User::factory(),
'approved_by_cashier_id' => User::factory(),
'cashier_approved_at' => now(),
'accountant_approved_by_id' => User::factory(),
'approved_by_accountant_id' => User::factory(),
'accountant_approved_at' => now(),
'chair_approved_by_id' => User::factory(),
'approved_by_chair_id' => User::factory(),
'chair_approved_at' => now(),
]);
}
@@ -120,7 +131,7 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'amount' => $this->faker->randomFloat(2, 5000, 50000),
'amount_tier' => 'medium',
'amount_tier' => null,
'requires_board_meeting' => false,
]);
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use App\Models\Issue;
use App\Models\IssueAttachment;
use Illuminate\Database\Eloquent\Factories\Factory;
class IssueAttachmentFactory extends Factory
{
protected $model = IssueAttachment::class;
public function definition(): array
{
return [
'issue_id' => Issue::factory(),
'user_id' => \App\Models\User::factory(),
'file_path' => 'issues/'.$this->faker->uuid.'.txt',
'file_name' => $this->faker->word.'.txt',
'file_size' => $this->faker->numberBetween(1000, 5000),
'mime_type' => 'text/plain',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use App\Models\Issue;
use App\Models\IssueComment;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class IssueCommentFactory extends Factory
{
protected $model = IssueComment::class;
public function definition(): array
{
return [
'issue_id' => Issue::factory(),
'user_id' => User::factory(),
'comment_text' => $this->faker->sentence(),
'is_internal' => false,
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\Issue;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class IssueFactory extends Factory
{
protected $model = Issue::class;
public function definition(): array
{
return [
'issue_number' => null, // auto-generated in model boot
'title' => $this->faker->sentence(),
'description' => $this->faker->paragraph(),
'issue_type' => Issue::TYPE_WORK_ITEM,
'status' => Issue::STATUS_NEW,
'priority' => Issue::PRIORITY_MEDIUM,
'created_by_user_id' => User::factory(),
'assigned_to_user_id' => null,
'reviewer_id' => null,
'member_id' => null,
'parent_issue_id' => null,
'due_date' => now()->addDays(7),
'estimated_hours' => 4,
'actual_hours' => 0,
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Database\Factories;
use App\Models\IssueLabel;
use Illuminate\Database\Eloquent\Factories\Factory;
class IssueLabelFactory extends Factory
{
protected $model = IssueLabel::class;
public function definition(): array
{
return [
'name' => $this->faker->unique()->word(),
'color' => $this->faker->safeHexColor(),
'description' => $this->faker->sentence(),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use App\Models\Issue;
use App\Models\IssueTimeLog;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class IssueTimeLogFactory extends Factory
{
protected $model = IssueTimeLog::class;
public function definition(): array
{
return [
'issue_id' => Issue::factory(),
'user_id' => User::factory(),
'hours' => $this->faker->randomFloat(1, 0.5, 8),
'description' => $this->faker->sentence(),
'logged_at' => now(),
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\Member;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class MemberFactory extends Factory
{
protected $model = Member::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'full_name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'phone' => $this->faker->numerify('09########'),
'membership_status' => Member::STATUS_PENDING,
'membership_type' => Member::TYPE_REGULAR,
'membership_started_at' => now()->subMonth(),
'membership_expires_at' => now()->addYear(),
];
}
public function active(): static
{
return $this->state(fn () => [
'membership_status' => Member::STATUS_ACTIVE,
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Database\Factories;
use App\Models\Member;
use App\Models\MembershipPayment;
use Illuminate\Database\Eloquent\Factories\Factory;
class MembershipPaymentFactory extends Factory
{
protected $model = MembershipPayment::class;
public function definition(): array
{
return [
'member_id' => Member::factory(),
'paid_at' => now()->format('Y-m-d'),
'amount' => $this->faker->numberBetween(500, 5000),
'method' => MembershipPayment::METHOD_BANK_TRANSFER,
'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER,
'reference' => $this->faker->bothify('REF-######'),
'status' => MembershipPayment::STATUS_PENDING,
'submitted_by_user_id' => null,
'notes' => $this->faker->sentence(),
];
}
public function approvedCashier(): static
{
return $this->state(fn () => [
'status' => MembershipPayment::STATUS_APPROVED_CASHIER,
'cashier_verified_at' => now(),
]);
}
public function approvedAccountant(): static
{
return $this->state(fn () => [
'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT,
'accountant_verified_at' => now(),
]);
}
public function approvedChair(): static
{
return $this->state(fn () => [
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
'chair_verified_at' => now(),
]);
}
}

View File

@@ -1,32 +0,0 @@
<?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

@@ -1,28 +0,0 @@
<?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

@@ -1,32 +0,0 @@
<?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

@@ -1,33 +0,0 @@
<?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

@@ -1,36 +0,0 @@
<?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

@@ -1,62 +0,0 @@
<?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

@@ -1,55 +0,0 @@
<?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

@@ -1,44 +0,0 @@
<?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

@@ -1,35 +0,0 @@
<?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

@@ -1,32 +0,0 @@
<?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

@@ -1,22 +0,0 @@
<?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

@@ -1,134 +0,0 @@
<?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

@@ -1,27 +0,0 @@
<?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

@@ -1,22 +0,0 @@
<?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

@@ -1,26 +0,0 @@
<?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

@@ -1,28 +0,0 @@
<?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

@@ -1,25 +0,0 @@
<?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

@@ -1,22 +0,0 @@
<?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

@@ -1,23 +0,0 @@
<?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

@@ -1,22 +0,0 @@
<?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

@@ -1,62 +0,0 @@
<?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

@@ -1,39 +0,0 @@
<?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

@@ -1,34 +0,0 @@
<?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

@@ -1,40 +0,0 @@
<?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

@@ -1,42 +0,0 @@
<?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

@@ -1,41 +0,0 @@
<?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

@@ -1,66 +0,0 @@
<?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

@@ -1,35 +0,0 @@
<?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

@@ -1,36 +0,0 @@
<?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

@@ -1,33 +0,0 @@
<?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

@@ -1,33 +0,0 @@
<?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

@@ -1,31 +0,0 @@
<?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

@@ -1,30 +0,0 @@
<?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

@@ -1,35 +0,0 @@
<?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

@@ -1,37 +0,0 @@
<?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

@@ -1,31 +0,0 @@
<?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

@@ -1,82 +0,0 @@
<?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

@@ -1,38 +0,0 @@
<?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

@@ -1,33 +0,0 @@
<?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

@@ -1,43 +0,0 @@
<?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

@@ -1,30 +0,0 @@
<?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

@@ -1,32 +0,0 @@
<?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

@@ -1,124 +0,0 @@
<?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

@@ -1,64 +0,0 @@
<?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

@@ -1,54 +0,0 @@
<?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

@@ -1,60 +0,0 @@
<?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');
}
};

View File

@@ -0,0 +1,156 @@
CREATE TABLE IF NOT EXISTS "migrations" ("id" integer primary key autoincrement not null, "migration" varchar not null, "batch" integer not null);
CREATE TABLE IF NOT EXISTS "users" ("id" integer primary key autoincrement not null, "name" varchar not null, "email" varchar not null, "email_verified_at" datetime, "password" varchar not null, "remember_token" varchar, "created_at" datetime, "updated_at" datetime, "is_admin" tinyint(1) not null default '0', "profile_photo_path" varchar);
CREATE UNIQUE INDEX "users_email_unique" on "users" ("email");
CREATE TABLE IF NOT EXISTS "password_reset_tokens" ("email" varchar not null, "token" varchar not null, "created_at" datetime, primary key ("email"));
CREATE TABLE IF NOT EXISTS "failed_jobs" ("id" integer primary key autoincrement not null, "uuid" varchar not null, "connection" text not null, "queue" text not null, "payload" text not null, "exception" text not null, "failed_at" datetime not null default CURRENT_TIMESTAMP);
CREATE UNIQUE INDEX "failed_jobs_uuid_unique" on "failed_jobs" ("uuid");
CREATE TABLE IF NOT EXISTS "personal_access_tokens" ("id" integer primary key autoincrement not null, "tokenable_type" varchar not null, "tokenable_id" integer not null, "name" varchar not null, "token" varchar not null, "abilities" text, "last_used_at" datetime, "expires_at" datetime, "created_at" datetime, "updated_at" datetime);
CREATE INDEX "personal_access_tokens_tokenable_type_tokenable_id_index" on "personal_access_tokens" ("tokenable_type", "tokenable_id");
CREATE UNIQUE INDEX "personal_access_tokens_token_unique" on "personal_access_tokens" ("token");
CREATE TABLE IF NOT EXISTS "document_categories" ("id" integer primary key autoincrement not null, "name" varchar not null, "slug" varchar not null, "description" text, "icon" varchar, "sort_order" integer not null default '0', "default_access_level" varchar check ("default_access_level" in ('public', 'members', 'admin', 'board')) not null default 'members', "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "document_categories_slug_unique" on "document_categories" ("slug");
CREATE TABLE IF NOT EXISTS "documents" ("id" integer primary key autoincrement not null, "document_category_id" integer not null, "title" varchar not null, "document_number" varchar, "description" text, "public_uuid" varchar not null, "access_level" varchar check ("access_level" in ('public', 'members', 'admin', 'board')) not null default 'members', "current_version_id" integer, "status" varchar check ("status" in ('active', 'archived')) not null default 'active', "archived_at" datetime, "created_by_user_id" integer not null, "last_updated_by_user_id" integer, "view_count" integer not null default '0', "download_count" integer not null default '0', "version_count" integer not null default '0', "created_at" datetime, "updated_at" datetime, "deleted_at" datetime, "expires_at" date, "auto_archive_on_expiry" tinyint(1) not null default '0', "expiry_notice" text, foreign key("document_category_id") references "document_categories"("id") on delete cascade, foreign key("current_version_id") references "document_versions"("id") on delete set null, foreign key("created_by_user_id") references "users"("id") on delete cascade, foreign key("last_updated_by_user_id") references "users"("id") on delete set null);
CREATE INDEX "documents_document_category_id_index" on "documents" ("document_category_id");
CREATE INDEX "documents_access_level_index" on "documents" ("access_level");
CREATE INDEX "documents_status_index" on "documents" ("status");
CREATE INDEX "documents_public_uuid_index" on "documents" ("public_uuid");
CREATE INDEX "documents_created_at_index" on "documents" ("created_at");
CREATE UNIQUE INDEX "documents_document_number_unique" on "documents" ("document_number");
CREATE UNIQUE INDEX "documents_public_uuid_unique" on "documents" ("public_uuid");
CREATE TABLE IF NOT EXISTS "document_versions" ("id" integer primary key autoincrement not null, "document_id" integer not null, "version_number" varchar not null, "version_notes" text, "is_current" tinyint(1) not null default '0', "file_path" varchar not null, "original_filename" varchar not null, "mime_type" varchar not null, "file_size" integer not null, "file_hash" varchar, "uploaded_by_user_id" integer not null, "uploaded_at" datetime not null, "created_at" datetime, "updated_at" datetime, foreign key("document_id") references "documents"("id") on delete cascade, foreign key("uploaded_by_user_id") references "users"("id") on delete cascade);
CREATE INDEX "document_versions_document_id_index" on "document_versions" ("document_id");
CREATE INDEX "document_versions_version_number_index" on "document_versions" ("version_number");
CREATE INDEX "document_versions_is_current_index" on "document_versions" ("is_current");
CREATE INDEX "document_versions_uploaded_at_index" on "document_versions" ("uploaded_at");
CREATE TABLE IF NOT EXISTS "document_access_logs" ("id" integer primary key autoincrement not null, "document_id" integer not null, "document_version_id" integer, "action" varchar check ("action" in ('view', 'download')) not null, "user_id" integer, "ip_address" varchar, "user_agent" text, "accessed_at" datetime not null, "created_at" datetime, "updated_at" datetime, foreign key("document_id") references "documents"("id") on delete cascade, foreign key("document_version_id") references "document_versions"("id") on delete set null, foreign key("user_id") references "users"("id") on delete set null);
CREATE INDEX "document_access_logs_document_id_index" on "document_access_logs" ("document_id");
CREATE INDEX "document_access_logs_user_id_index" on "document_access_logs" ("user_id");
CREATE INDEX "document_access_logs_action_index" on "document_access_logs" ("action");
CREATE INDEX "document_access_logs_accessed_at_index" on "document_access_logs" ("accessed_at");
CREATE TABLE IF NOT EXISTS "members" ("id" integer primary key autoincrement not null, "user_id" integer, "full_name" varchar not null, "email" varchar not null, "phone" varchar, "national_id_encrypted" varchar, "national_id_hash" varchar, "membership_started_at" date, "membership_expires_at" date, "created_at" datetime, "updated_at" datetime, "last_expiry_reminder_sent_at" datetime, "address_line_1" varchar, "address_line_2" varchar, "city" varchar, "postal_code" varchar, "emergency_contact_name" varchar, "emergency_contact_phone" varchar, "membership_status" varchar check ("membership_status" in ('pending', 'active', 'expired', 'suspended')) not null default 'pending', "membership_type" varchar check ("membership_type" in ('regular', 'honorary', 'lifetime', 'student')) not null default 'regular', foreign key("user_id") references "users"("id") on delete set null);
CREATE INDEX "members_email_index" on "members" ("email");
CREATE INDEX "members_national_id_hash_index" on "members" ("national_id_hash");
CREATE TABLE IF NOT EXISTS "membership_payments" ("id" integer primary key autoincrement not null, "member_id" integer not null, "paid_at" date not null, "amount" numeric not null, "method" varchar, "reference" varchar, "created_at" datetime, "updated_at" datetime, "status" varchar check ("status" in ('pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected')) not null default 'pending', "payment_method" varchar check ("payment_method" in ('bank_transfer', 'convenience_store', 'cash', 'credit_card')), "receipt_path" varchar, "submitted_by_user_id" integer, "verified_by_cashier_id" integer, "cashier_verified_at" datetime, "verified_by_accountant_id" integer, "accountant_verified_at" datetime, "verified_by_chair_id" integer, "chair_verified_at" datetime, "rejected_by_user_id" integer, "rejected_at" datetime, "rejection_reason" text, "notes" text, foreign key("member_id") references "members"("id") on delete cascade);
CREATE TABLE IF NOT EXISTS "permissions" ("id" integer primary key autoincrement not null, "name" varchar not null, "guard_name" varchar not null, "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "permissions_name_guard_name_unique" on "permissions" ("name", "guard_name");
CREATE TABLE IF NOT EXISTS "roles" ("id" integer primary key autoincrement not null, "name" varchar not null, "guard_name" varchar not null, "created_at" datetime, "updated_at" datetime, "description" varchar);
CREATE UNIQUE INDEX "roles_name_guard_name_unique" on "roles" ("name", "guard_name");
CREATE TABLE IF NOT EXISTS "model_has_permissions" ("permission_id" integer not null, "model_type" varchar not null, "model_id" integer not null, foreign key("permission_id") references "permissions"("id") on delete cascade, primary key ("permission_id", "model_id", "model_type"));
CREATE INDEX "model_has_permissions_model_id_model_type_index" on "model_has_permissions" ("model_id", "model_type");
CREATE TABLE IF NOT EXISTS "model_has_roles" ("role_id" integer not null, "model_type" varchar not null, "model_id" integer not null, foreign key("role_id") references "roles"("id") on delete cascade, primary key ("role_id", "model_id", "model_type"));
CREATE INDEX "model_has_roles_model_id_model_type_index" on "model_has_roles" ("model_id", "model_type");
CREATE TABLE IF NOT EXISTS "role_has_permissions" ("permission_id" integer not null, "role_id" integer not null, foreign key("permission_id") references "permissions"("id") on delete cascade, foreign key("role_id") references "roles"("id") on delete cascade, primary key ("permission_id", "role_id"));
CREATE TABLE IF NOT EXISTS "audit_logs" ("id" integer primary key autoincrement not null, "user_id" integer, "action" varchar not null, "auditable_type" varchar, "auditable_id" integer, "metadata" text, "created_at" datetime, "updated_at" datetime, foreign key("user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "finance_documents" ("id" integer primary key autoincrement not null, "member_id" integer, "submitted_by_user_id" integer, "title" varchar not null, "amount" numeric, "status" varchar not null default 'pending', "description" text, "submitted_at" datetime, "created_at" datetime, "updated_at" datetime, "attachment_path" varchar, "approved_by_cashier_id" integer, "cashier_approved_at" datetime, "approved_by_accountant_id" integer, "accountant_approved_at" datetime, "approved_by_chair_id" integer, "chair_approved_at" datetime, "rejected_by_user_id" integer, "rejected_at" datetime, "rejection_reason" text, "submitted_by_id" integer, "request_type" varchar check ("request_type" in ('expense_reimbursement', 'advance_payment', 'purchase_request', 'petty_cash')) not null default 'expense_reimbursement', "amount_tier" varchar check ("amount_tier" in ('small', 'medium', 'large')), "chart_of_account_id" integer, "budget_item_id" integer, "requires_board_meeting" tinyint(1) not null default '0', "board_meeting_date" date, "board_meeting_decision" text, "approved_by_board_meeting_id" integer, "board_meeting_approved_at" datetime, "payment_order_created_by_accountant_id" integer, "payment_order_created_at" datetime, "payment_method" varchar check ("payment_method" in ('bank_transfer', 'check', 'cash')), "payee_name" varchar, "payee_bank_code" varchar, "payee_account_number" varchar, "payee_bank_name" varchar, "payment_notes" text, "payment_verified_by_cashier_id" integer, "payment_verified_at" datetime, "payment_verification_notes" text, "payment_executed_by_cashier_id" integer, "payment_executed_at" datetime, "payment_transaction_id" varchar, "payment_receipt_path" varchar, "actual_payment_amount" numeric, "cashier_ledger_entry_id" integer, "cashier_recorded_at" datetime, "accounting_transaction_id" integer, "accountant_recorded_at" datetime, "bank_reconciliation_id" integer, "reconciliation_status" varchar check ("reconciliation_status" in ('pending', 'matched', 'discrepancy', 'resolved')) not null default 'pending', "reconciliation_notes" text, "reconciled_at" datetime, "reconciled_by_user_id" integer, foreign key("member_id") references "members"("id") on delete set null, foreign key("submitted_by_user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "chart_of_accounts" ("id" integer primary key autoincrement not null, "account_code" varchar not null, "account_name_zh" varchar not null, "account_name_en" varchar, "account_type" varchar check ("account_type" in ('asset', 'liability', 'net_asset', 'income', 'expense')) not null, "category" varchar, "parent_account_id" integer, "is_active" tinyint(1) not null default '1', "display_order" integer not null default '0', "description" text, "created_at" datetime, "updated_at" datetime, foreign key("parent_account_id") references "chart_of_accounts"("id") on delete set null);
CREATE INDEX "chart_of_accounts_account_type_index" on "chart_of_accounts" ("account_type");
CREATE INDEX "chart_of_accounts_is_active_index" on "chart_of_accounts" ("is_active");
CREATE UNIQUE INDEX "chart_of_accounts_account_code_unique" on "chart_of_accounts" ("account_code");
CREATE TABLE IF NOT EXISTS "budget_items" ("id" integer primary key autoincrement not null, "budget_id" integer not null, "chart_of_account_id" integer not null, "budgeted_amount" numeric not null default '0', "actual_amount" numeric not null default '0', "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("budget_id") references "budgets"("id") on delete cascade, foreign key("chart_of_account_id") references "chart_of_accounts"("id") on delete cascade);
CREATE INDEX "budget_items_budget_id_chart_of_account_id_index" on "budget_items" ("budget_id", "chart_of_account_id");
CREATE TABLE IF NOT EXISTS "budgets" ("id" integer primary key autoincrement not null, "fiscal_year" integer not null, "name" varchar not null, "period_type" varchar check ("period_type" in ('annual', 'quarterly', 'monthly')) not null default 'annual', "period_start" date not null, "period_end" date not null, "status" varchar check ("status" in ('draft', 'submitted', 'approved', 'active', 'closed')) not null default 'draft', "created_by_user_id" integer not null, "approved_by_user_id" integer, "approved_at" datetime, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("created_by_user_id") references "users"("id") on delete cascade, foreign key("approved_by_user_id") references "users"("id") on delete set null);
CREATE INDEX "budgets_fiscal_year_index" on "budgets" ("fiscal_year");
CREATE INDEX "budgets_status_index" on "budgets" ("status");
CREATE TABLE IF NOT EXISTS "transactions" ("id" integer primary key autoincrement not null, "budget_item_id" integer, "chart_of_account_id" integer not null, "transaction_date" date not null, "amount" numeric not null, "transaction_type" varchar check ("transaction_type" in ('income', 'expense')) not null, "description" varchar not null, "reference_number" varchar, "finance_document_id" integer, "membership_payment_id" integer, "created_by_user_id" integer not null, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("budget_item_id") references "budget_items"("id") on delete set null, foreign key("chart_of_account_id") references "chart_of_accounts"("id") on delete cascade, foreign key("finance_document_id") references "finance_documents"("id") on delete set null, foreign key("membership_payment_id") references "membership_payments"("id") on delete set null, foreign key("created_by_user_id") references "users"("id") on delete cascade);
CREATE INDEX "transactions_transaction_date_index" on "transactions" ("transaction_date");
CREATE INDEX "transactions_transaction_type_index" on "transactions" ("transaction_type");
CREATE INDEX "transactions_budget_item_id_transaction_date_index" on "transactions" ("budget_item_id", "transaction_date");
CREATE TABLE IF NOT EXISTS "financial_reports" ("id" integer primary key autoincrement not null, "report_type" varchar check ("report_type" in ('revenue_expenditure', 'balance_sheet', 'property_inventory', 'internal_management')) not null, "fiscal_year" integer not null, "period_start" date not null, "period_end" date not null, "status" varchar check ("status" in ('draft', 'finalized', 'approved', 'submitted')) not null default 'draft', "budget_id" integer, "generated_by_user_id" integer not null, "approved_by_user_id" integer, "approved_at" datetime, "file_path" varchar, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("budget_id") references "budgets"("id") on delete set null, foreign key("generated_by_user_id") references "users"("id") on delete cascade, foreign key("approved_by_user_id") references "users"("id") on delete set null);
CREATE INDEX "financial_reports_report_type_fiscal_year_index" on "financial_reports" ("report_type", "fiscal_year");
CREATE INDEX "financial_reports_status_index" on "financial_reports" ("status");
CREATE TABLE IF NOT EXISTS "issues" ("id" integer primary key autoincrement not null, "issue_number" varchar not null, "title" varchar not null, "description" text, "issue_type" varchar check ("issue_type" in ('work_item', 'project_task', 'maintenance', 'member_request')) not null default 'work_item', "status" varchar check ("status" in ('new', 'assigned', 'in_progress', 'review', 'closed')) not null default 'new', "priority" varchar check ("priority" in ('low', 'medium', 'high', 'urgent')) not null default 'medium', "created_by_user_id" integer not null, "assigned_to_user_id" integer, "reviewer_id" integer, "member_id" integer, "parent_issue_id" integer, "due_date" date, "closed_at" datetime, "estimated_hours" numeric, "actual_hours" numeric not null default '0', "created_at" datetime, "updated_at" datetime, "deleted_at" datetime, foreign key("created_by_user_id") references "users"("id") on delete cascade, foreign key("assigned_to_user_id") references "users"("id") on delete set null, foreign key("reviewer_id") references "users"("id") on delete set null, foreign key("member_id") references "members"("id") on delete set null, foreign key("parent_issue_id") references "issues"("id") on delete set null);
CREATE INDEX "issues_issue_type_index" on "issues" ("issue_type");
CREATE INDEX "issues_status_index" on "issues" ("status");
CREATE INDEX "issues_priority_index" on "issues" ("priority");
CREATE INDEX "issues_assigned_to_user_id_index" on "issues" ("assigned_to_user_id");
CREATE INDEX "issues_created_by_user_id_index" on "issues" ("created_by_user_id");
CREATE INDEX "issues_due_date_index" on "issues" ("due_date");
CREATE UNIQUE INDEX "issues_issue_number_unique" on "issues" ("issue_number");
CREATE TABLE IF NOT EXISTS "issue_comments" ("id" integer primary key autoincrement not null, "issue_id" integer not null, "user_id" integer not null, "comment_text" text not null, "is_internal" tinyint(1) not null default '0', "created_at" datetime, "updated_at" datetime, foreign key("issue_id") references "issues"("id") on delete cascade, foreign key("user_id") references "users"("id") on delete cascade);
CREATE INDEX "issue_comments_issue_id_index" on "issue_comments" ("issue_id");
CREATE INDEX "issue_comments_user_id_index" on "issue_comments" ("user_id");
CREATE TABLE IF NOT EXISTS "issue_attachments" ("id" integer primary key autoincrement not null, "issue_id" integer not null, "user_id" integer not null, "file_name" varchar not null, "file_path" varchar not null, "file_size" integer not null, "mime_type" varchar not null, "created_at" datetime, "updated_at" datetime, foreign key("issue_id") references "issues"("id") on delete cascade, foreign key("user_id") references "users"("id") on delete cascade);
CREATE INDEX "issue_attachments_issue_id_index" on "issue_attachments" ("issue_id");
CREATE TABLE IF NOT EXISTS "custom_field_values" ("id" integer primary key autoincrement not null, "custom_field_id" integer not null, "customizable_type" varchar not null, "customizable_id" integer not null, "value" text not null, "created_at" datetime, "updated_at" datetime, foreign key("custom_field_id") references "custom_fields"("id") on delete cascade);
CREATE INDEX "custom_field_values_customizable_type_customizable_id_index" on "custom_field_values" ("customizable_type", "customizable_id");
CREATE INDEX "custom_field_values_custom_field_id_index" on "custom_field_values" ("custom_field_id");
CREATE TABLE IF NOT EXISTS "custom_fields" ("id" integer primary key autoincrement not null, "name" varchar not null, "field_type" varchar check ("field_type" in ('text', 'number', 'date', 'select')) not null, "options" text, "applies_to_issue_types" text not null, "is_required" tinyint(1) not null default '0', "display_order" integer not null default '0', "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "custom_fields_name_unique" on "custom_fields" ("name");
CREATE TABLE IF NOT EXISTS "issue_label_pivot" ("issue_id" integer not null, "issue_label_id" integer not null, "created_at" datetime, "updated_at" datetime, foreign key("issue_id") references "issues"("id") on delete cascade, foreign key("issue_label_id") references "issue_labels"("id") on delete cascade, primary key ("issue_id", "issue_label_id"));
CREATE TABLE IF NOT EXISTS "issue_labels" ("id" integer primary key autoincrement not null, "name" varchar not null, "color" varchar not null default '#6B7280', "description" text, "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "issue_labels_name_unique" on "issue_labels" ("name");
CREATE TABLE IF NOT EXISTS "issue_relationships" ("id" integer primary key autoincrement not null, "issue_id" integer not null, "related_issue_id" integer not null, "relationship_type" varchar check ("relationship_type" in ('blocks', 'blocked_by', 'related_to', 'duplicate_of')) not null, "created_at" datetime, "updated_at" datetime, foreign key("issue_id") references "issues"("id") on delete cascade, foreign key("related_issue_id") references "issues"("id") on delete cascade);
CREATE INDEX "issue_relationships_issue_id_index" on "issue_relationships" ("issue_id");
CREATE INDEX "issue_relationships_related_issue_id_index" on "issue_relationships" ("related_issue_id");
CREATE TABLE IF NOT EXISTS "issue_time_logs" ("id" integer primary key autoincrement not null, "issue_id" integer not null, "user_id" integer not null, "hours" numeric not null, "description" text, "logged_at" datetime not null, "created_at" datetime, "updated_at" datetime, foreign key("issue_id") references "issues"("id") on delete cascade, foreign key("user_id") references "users"("id") on delete cascade);
CREATE INDEX "issue_time_logs_issue_id_index" on "issue_time_logs" ("issue_id");
CREATE INDEX "issue_time_logs_user_id_index" on "issue_time_logs" ("user_id");
CREATE INDEX "issue_time_logs_logged_at_index" on "issue_time_logs" ("logged_at");
CREATE TABLE IF NOT EXISTS "issue_watchers" ("issue_id" integer not null, "user_id" integer not null, "created_at" datetime, "updated_at" datetime, foreign key("issue_id") references "issues"("id") on delete cascade, foreign key("user_id") references "users"("id") on delete cascade, primary key ("issue_id", "user_id"));
CREATE TABLE IF NOT EXISTS "document_tags" ("id" integer primary key autoincrement not null, "name" varchar not null, "slug" varchar not null, "color" varchar not null default '#6366f1', "description" text, "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "document_tags_slug_unique" on "document_tags" ("slug");
CREATE TABLE IF NOT EXISTS "document_document_tag" ("id" integer primary key autoincrement not null, "document_id" integer not null, "document_tag_id" integer not null, "created_at" datetime, "updated_at" datetime, foreign key("document_id") references "documents"("id") on delete cascade, foreign key("document_tag_id") references "document_tags"("id") on delete cascade);
CREATE UNIQUE INDEX "document_document_tag_document_id_document_tag_id_unique" on "document_document_tag" ("document_id", "document_tag_id");
CREATE TABLE IF NOT EXISTS "system_settings" ("id" integer primary key autoincrement not null, "key" varchar not null, "value" text, "type" varchar check ("type" in ('string', 'integer', 'boolean', 'json', 'array')) not null default 'string', "group" varchar, "description" text, "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "system_settings_key_unique" on "system_settings" ("key");
CREATE INDEX "system_settings_group_index" on "system_settings" ("group");
CREATE TABLE IF NOT EXISTS "payment_orders" ("id" integer primary key autoincrement not null, "finance_document_id" integer not null, "payee_name" varchar not null, "payee_bank_code" varchar, "payee_account_number" varchar, "payee_bank_name" varchar, "payment_amount" numeric not null, "payment_method" varchar check ("payment_method" in ('bank_transfer', 'check', 'cash')) not null, "created_by_accountant_id" integer not null, "payment_order_number" varchar not null, "notes" text, "verified_by_cashier_id" integer, "verified_at" datetime, "verification_status" varchar check ("verification_status" in ('pending', 'approved', 'rejected')) not null default 'pending', "verification_notes" text, "executed_by_cashier_id" integer, "executed_at" datetime, "execution_status" varchar check ("execution_status" in ('pending', 'completed', 'failed')) not null default 'pending', "transaction_reference" varchar, "payment_receipt_path" varchar, "status" varchar check ("status" in ('draft', 'pending_verification', 'verified', 'executed', 'cancelled')) not null default 'draft', "created_at" datetime, "updated_at" datetime, foreign key("finance_document_id") references "finance_documents"("id") on delete cascade, foreign key("created_by_accountant_id") references "users"("id") on delete cascade, foreign key("verified_by_cashier_id") references "users"("id") on delete set null, foreign key("executed_by_cashier_id") references "users"("id") on delete set null);
CREATE INDEX "payment_orders_finance_document_id_index" on "payment_orders" ("finance_document_id");
CREATE INDEX "payment_orders_status_index" on "payment_orders" ("status");
CREATE INDEX "payment_orders_verification_status_index" on "payment_orders" ("verification_status");
CREATE INDEX "payment_orders_execution_status_index" on "payment_orders" ("execution_status");
CREATE UNIQUE INDEX "payment_orders_payment_order_number_unique" on "payment_orders" ("payment_order_number");
CREATE TABLE IF NOT EXISTS "cashier_ledger_entries" ("id" integer primary key autoincrement not null, "finance_document_id" integer, "entry_date" date not null, "entry_type" varchar check ("entry_type" in ('receipt', 'payment')) not null, "payment_method" varchar check ("payment_method" in ('bank_transfer', 'check', 'cash')) not null, "bank_account" varchar, "amount" numeric not null, "balance_before" numeric not null, "balance_after" numeric not null, "receipt_number" varchar, "transaction_reference" varchar, "recorded_by_cashier_id" integer not null, "recorded_at" datetime not null default CURRENT_TIMESTAMP, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("finance_document_id") references "finance_documents"("id") on delete cascade, foreign key("recorded_by_cashier_id") references "users"("id") on delete cascade);
CREATE INDEX "cashier_ledger_entries_finance_document_id_index" on "cashier_ledger_entries" ("finance_document_id");
CREATE INDEX "cashier_ledger_entries_entry_date_index" on "cashier_ledger_entries" ("entry_date");
CREATE INDEX "cashier_ledger_entries_entry_type_index" on "cashier_ledger_entries" ("entry_type");
CREATE INDEX "cashier_ledger_entries_recorded_by_cashier_id_index" on "cashier_ledger_entries" ("recorded_by_cashier_id");
CREATE TABLE IF NOT EXISTS "bank_reconciliations" ("id" integer primary key autoincrement not null, "reconciliation_month" date not null, "bank_statement_balance" numeric not null, "bank_statement_date" date not null, "bank_statement_file_path" varchar, "system_book_balance" numeric not null, "outstanding_checks" text, "deposits_in_transit" text, "bank_charges" text, "adjusted_balance" numeric not null, "discrepancy_amount" numeric not null default '0', "reconciliation_status" varchar check ("reconciliation_status" in ('pending', 'completed', 'discrepancy')) not null default 'pending', "prepared_by_cashier_id" integer not null, "reviewed_by_accountant_id" integer, "approved_by_manager_id" integer, "prepared_at" datetime not null default CURRENT_TIMESTAMP, "reviewed_at" datetime, "approved_at" datetime, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("prepared_by_cashier_id") references "users"("id") on delete cascade, foreign key("reviewed_by_accountant_id") references "users"("id") on delete set null, foreign key("approved_by_manager_id") references "users"("id") on delete set null);
CREATE INDEX "bank_reconciliations_reconciliation_month_index" on "bank_reconciliations" ("reconciliation_month");
CREATE INDEX "bank_reconciliations_reconciliation_status_index" on "bank_reconciliations" ("reconciliation_status");
INSERT INTO migrations VALUES(1,'2014_10_12_000000_create_users_table',1);
INSERT INTO migrations VALUES(2,'2014_10_12_100000_create_password_reset_tokens_table',1);
INSERT INTO migrations VALUES(3,'2019_08_19_000000_create_failed_jobs_table',1);
INSERT INTO migrations VALUES(4,'2019_12_14_000001_create_personal_access_tokens_table',1);
INSERT INTO migrations VALUES(5,'2024_01_20_100000_create_document_categories_table',1);
INSERT INTO migrations VALUES(6,'2024_01_20_100001_create_documents_table',1);
INSERT INTO migrations VALUES(7,'2024_01_20_100002_create_document_versions_table',1);
INSERT INTO migrations VALUES(8,'2024_01_20_100003_create_document_access_logs_table',1);
INSERT INTO migrations VALUES(9,'2025_01_01_000000_create_members_table',1);
INSERT INTO migrations VALUES(10,'2025_01_01_000100_create_membership_payments_table',1);
INSERT INTO migrations VALUES(11,'2025_01_01_000200_add_is_admin_to_users_table',1);
INSERT INTO migrations VALUES(12,'2025_11_18_083552_create_permission_tables',1);
INSERT INTO migrations VALUES(13,'2025_11_18_090000_migrate_is_admin_to_roles',1);
INSERT INTO migrations VALUES(14,'2025_11_18_091000_add_last_expiry_reminder_to_members_table',1);
INSERT INTO migrations VALUES(15,'2025_11_18_092000_create_audit_logs_table',1);
INSERT INTO migrations VALUES(16,'2025_11_18_093000_create_finance_documents_table',1);
INSERT INTO migrations VALUES(17,'2025_11_18_094000_add_address_fields_to_members_table',1);
INSERT INTO migrations VALUES(18,'2025_11_18_100000_add_description_to_roles_table',1);
INSERT INTO migrations VALUES(19,'2025_11_18_101000_add_emergency_contact_to_members_table',1);
INSERT INTO migrations VALUES(20,'2025_11_18_102000_add_profile_photo_to_users_table',1);
INSERT INTO migrations VALUES(21,'2025_11_19_125201_add_approval_fields_to_finance_documents_table',1);
INSERT INTO migrations VALUES(22,'2025_11_19_133704_create_chart_of_accounts_table',1);
INSERT INTO migrations VALUES(23,'2025_11_19_133732_create_budget_items_table',1);
INSERT INTO migrations VALUES(24,'2025_11_19_133732_create_budgets_table',1);
INSERT INTO migrations VALUES(25,'2025_11_19_133802_create_transactions_table',1);
INSERT INTO migrations VALUES(26,'2025_11_19_133828_create_financial_reports_table',1);
INSERT INTO migrations VALUES(27,'2025_11_19_144027_create_issues_table',1);
INSERT INTO migrations VALUES(28,'2025_11_19_144059_create_issue_comments_table',1);
INSERT INTO migrations VALUES(29,'2025_11_19_144129_create_issue_attachments_table',1);
INSERT INTO migrations VALUES(30,'2025_11_19_144130_create_custom_field_values_table',1);
INSERT INTO migrations VALUES(31,'2025_11_19_144130_create_custom_fields_table',1);
INSERT INTO migrations VALUES(32,'2025_11_19_144130_create_issue_label_pivot_table',1);
INSERT INTO migrations VALUES(33,'2025_11_19_144130_create_issue_labels_table',1);
INSERT INTO migrations VALUES(34,'2025_11_19_144130_create_issue_relationships_table',1);
INSERT INTO migrations VALUES(35,'2025_11_19_144130_create_issue_time_logs_table',1);
INSERT INTO migrations VALUES(36,'2025_11_19_144130_create_issue_watchers_table',1);
INSERT INTO migrations VALUES(37,'2025_11_19_155725_enhance_membership_payments_table_for_verification',1);
INSERT INTO migrations VALUES(38,'2025_11_19_155807_add_membership_status_to_members_table',1);
INSERT INTO migrations VALUES(39,'2025_11_20_080537_remove_unique_constraint_from_document_versions',1);
INSERT INTO migrations VALUES(40,'2025_11_20_084936_create_document_tags_table',1);
INSERT INTO migrations VALUES(41,'2025_11_20_085035_add_expiration_to_documents_table',1);
INSERT INTO migrations VALUES(42,'2025_11_20_095222_create_system_settings_table',1);
INSERT INTO migrations VALUES(43,'2025_11_20_125121_add_payment_stage_fields_to_finance_documents_table',1);
INSERT INTO migrations VALUES(44,'2025_11_20_125246_create_payment_orders_table',1);
INSERT INTO migrations VALUES(45,'2025_11_20_125247_create_cashier_ledger_entries_table',1);
INSERT INTO migrations VALUES(46,'2025_11_20_125249_create_bank_reconciliations_table',1);