Initial commit
This commit is contained in:
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
152
database/factories/CashierLedgerEntryFactory.php
Normal file
152
database/factories/CashierLedgerEntryFactory.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\CashierLedgerEntry;
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\CashierLedgerEntry>
|
||||
*/
|
||||
class CashierLedgerEntryFactory extends Factory
|
||||
{
|
||||
protected $model = CashierLedgerEntry::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$entryTypes = ['receipt', 'payment'];
|
||||
$paymentMethods = ['cash', 'bank_transfer', 'check'];
|
||||
$entryType = $this->faker->randomElement($entryTypes);
|
||||
$amount = $this->faker->randomFloat(2, 100, 50000);
|
||||
$balanceBefore = $this->faker->randomFloat(2, 0, 100000);
|
||||
|
||||
// Calculate balance after based on entry type
|
||||
$balanceAfter = $entryType === 'receipt'
|
||||
? $balanceBefore + $amount
|
||||
: $balanceBefore - $amount;
|
||||
|
||||
return [
|
||||
'entry_type' => $entryType,
|
||||
'entry_date' => $this->faker->dateTimeBetween('-30 days', 'now'),
|
||||
'amount' => $amount,
|
||||
'payment_method' => $this->faker->randomElement($paymentMethods),
|
||||
'bank_account' => $this->faker->company() . ' Bank - ' . $this->faker->numerify('##########'),
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'recorded_by_cashier_id' => User::factory(),
|
||||
'recorded_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the entry is a receipt (incoming money).
|
||||
*/
|
||||
public function receipt(): static
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$balanceBefore = $attributes['balance_before'] ?? 0;
|
||||
$amount = $attributes['amount'];
|
||||
|
||||
return [
|
||||
'entry_type' => 'receipt',
|
||||
'balance_after' => $balanceBefore + $amount,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the entry is a payment (outgoing money).
|
||||
*/
|
||||
public function payment(): static
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$balanceBefore = $attributes['balance_before'] ?? 0;
|
||||
$amount = $attributes['amount'];
|
||||
|
||||
return [
|
||||
'entry_type' => 'payment',
|
||||
'balance_after' => $balanceBefore - $amount,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the entry is linked to a finance document.
|
||||
*/
|
||||
public function withFinanceDocument(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'finance_document_id' => FinanceDocument::factory(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment method is cash.
|
||||
*/
|
||||
public function cash(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_method' => 'cash',
|
||||
'bank_account' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment method is bank transfer.
|
||||
*/
|
||||
public function bankTransfer(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_method' => 'bank_transfer',
|
||||
'bank_account' => $this->faker->company() . ' Bank - ' . $this->faker->numerify('##########'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment method is check.
|
||||
*/
|
||||
public function check(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_method' => 'check',
|
||||
'transaction_reference' => 'CHK' . $this->faker->numerify('######'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sequence of entries with running balance for a specific account.
|
||||
*/
|
||||
public function sequence(string $bankAccount, float $initialBalance = 0): static
|
||||
{
|
||||
static $currentBalance;
|
||||
|
||||
if ($currentBalance === null) {
|
||||
$currentBalance = $initialBalance;
|
||||
}
|
||||
|
||||
return $this->state(function (array $attributes) use ($bankAccount, &$currentBalance) {
|
||||
$amount = $attributes['amount'];
|
||||
$entryType = $attributes['entry_type'];
|
||||
$balanceBefore = $currentBalance;
|
||||
|
||||
$balanceAfter = $entryType === 'receipt'
|
||||
? $balanceBefore + $amount
|
||||
: $balanceBefore - $amount;
|
||||
|
||||
$currentBalance = $balanceAfter;
|
||||
|
||||
return [
|
||||
'bank_account' => $bankAccount,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
216
database/factories/FinanceDocumentFactory.php
Normal file
216
database/factories/FinanceDocumentFactory.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\FinanceDocument>
|
||||
*/
|
||||
class FinanceDocumentFactory extends Factory
|
||||
{
|
||||
protected $model = FinanceDocument::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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,
|
||||
'request_type' => $this->faker->randomElement($requestTypes),
|
||||
'status' => $this->faker->randomElement($statuses),
|
||||
'submitted_by_id' => User::factory(),
|
||||
'submitted_at' => now(),
|
||||
'amount_tier' => $this->determineAmountTier($amount),
|
||||
'requires_board_meeting' => $amount > 50000,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is pending approval.
|
||||
*/
|
||||
public function pending(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is approved by cashier.
|
||||
*/
|
||||
public function approvedByCashier(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
|
||||
'cashier_approved_by_id' => User::factory(),
|
||||
'cashier_approved_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is approved by accountant.
|
||||
*/
|
||||
public function approvedByAccountant(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
|
||||
'cashier_approved_by_id' => User::factory(),
|
||||
'cashier_approved_at' => now(),
|
||||
'accountant_approved_by_id' => User::factory(),
|
||||
'accountant_approved_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is approved by chair.
|
||||
*/
|
||||
public function approvedByChair(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
|
||||
'cashier_approved_by_id' => User::factory(),
|
||||
'cashier_approved_at' => now(),
|
||||
'accountant_approved_by_id' => User::factory(),
|
||||
'accountant_approved_at' => now(),
|
||||
'chair_approved_by_id' => User::factory(),
|
||||
'chair_approved_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is rejected.
|
||||
*/
|
||||
public function rejected(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => FinanceDocument::STATUS_REJECTED,
|
||||
'rejection_reason' => $this->faker->sentence(10),
|
||||
'rejected_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is a small amount (< 5000).
|
||||
*/
|
||||
public function smallAmount(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'amount' => $this->faker->randomFloat(2, 100, 4999),
|
||||
'amount_tier' => 'small',
|
||||
'requires_board_meeting' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is a medium amount (5000-50000).
|
||||
*/
|
||||
public function mediumAmount(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'amount' => $this->faker->randomFloat(2, 5000, 50000),
|
||||
'amount_tier' => 'medium',
|
||||
'requires_board_meeting' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is a large amount (> 50000).
|
||||
*/
|
||||
public function largeAmount(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'amount' => $this->faker->randomFloat(2, 50001, 200000),
|
||||
'amount_tier' => 'large',
|
||||
'requires_board_meeting' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is an expense reimbursement.
|
||||
*/
|
||||
public function expenseReimbursement(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'request_type' => 'expense_reimbursement',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is an advance payment.
|
||||
*/
|
||||
public function advancePayment(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'request_type' => 'advance_payment',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is a purchase request.
|
||||
*/
|
||||
public function purchaseRequest(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'request_type' => 'purchase_request',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the document is petty cash.
|
||||
*/
|
||||
public function pettyCash(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'request_type' => 'petty_cash',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that payment order has been created.
|
||||
*/
|
||||
public function withPaymentOrder(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_order_created_at' => now(),
|
||||
'payment_order_created_by_id' => User::factory(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that payment has been executed.
|
||||
*/
|
||||
public function paymentExecuted(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_order_created_at' => now(),
|
||||
'payment_verified_at' => now(),
|
||||
'payment_executed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine amount tier based on amount.
|
||||
*/
|
||||
protected function determineAmountTier(float $amount): string
|
||||
{
|
||||
if ($amount < 5000) {
|
||||
return 'small';
|
||||
} elseif ($amount <= 50000) {
|
||||
return 'medium';
|
||||
} else {
|
||||
return 'large';
|
||||
}
|
||||
}
|
||||
}
|
||||
152
database/factories/PaymentOrderFactory.php
Normal file
152
database/factories/PaymentOrderFactory.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\PaymentOrder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\PaymentOrder>
|
||||
*/
|
||||
class PaymentOrderFactory extends Factory
|
||||
{
|
||||
protected $model = PaymentOrder::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$paymentMethods = ['cash', 'check', 'bank_transfer'];
|
||||
$paymentMethod = $this->faker->randomElement($paymentMethods);
|
||||
|
||||
$attributes = [
|
||||
'finance_document_id' => FinanceDocument::factory(),
|
||||
'payment_order_number' => PaymentOrder::generatePaymentOrderNumber(),
|
||||
'payee_name' => $this->faker->name(),
|
||||
'payment_amount' => $this->faker->randomFloat(2, 100, 50000),
|
||||
'payment_method' => $paymentMethod,
|
||||
'status' => 'pending_verification',
|
||||
'verification_status' => 'pending',
|
||||
'execution_status' => 'pending',
|
||||
'created_by_accountant_id' => User::factory(),
|
||||
];
|
||||
|
||||
// Add bank details for bank transfers
|
||||
if ($paymentMethod === 'bank_transfer') {
|
||||
$attributes['payee_bank_name'] = $this->faker->company() . ' Bank';
|
||||
$attributes['payee_bank_code'] = $this->faker->numerify('###');
|
||||
$attributes['payee_account_number'] = $this->faker->numerify('##########');
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment order is pending verification.
|
||||
*/
|
||||
public function pendingVerification(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'pending_verification',
|
||||
'verification_status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment order is verified.
|
||||
*/
|
||||
public function verified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'verified',
|
||||
'verification_status' => 'approved',
|
||||
'verified_by_cashier_id' => User::factory(),
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment order is rejected during verification.
|
||||
*/
|
||||
public function rejectedDuringVerification(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'pending_verification',
|
||||
'verification_status' => 'rejected',
|
||||
'verified_by_cashier_id' => User::factory(),
|
||||
'verified_at' => now(),
|
||||
'verification_notes' => $this->faker->sentence(10),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment order is executed.
|
||||
*/
|
||||
public function executed(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'executed',
|
||||
'verification_status' => 'approved',
|
||||
'execution_status' => 'completed',
|
||||
'verified_by_cashier_id' => User::factory(),
|
||||
'verified_at' => now()->subHours(2),
|
||||
'executed_by_cashier_id' => User::factory(),
|
||||
'executed_at' => now(),
|
||||
'transaction_reference' => 'TXN' . $this->faker->numerify('##########'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment order is cancelled.
|
||||
*/
|
||||
public function cancelled(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => 'cancelled',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment method is cash.
|
||||
*/
|
||||
public function cash(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_method' => 'cash',
|
||||
'payee_bank_name' => null,
|
||||
'payee_bank_code' => null,
|
||||
'payee_account_number' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment method is check.
|
||||
*/
|
||||
public function check(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_method' => 'check',
|
||||
'payee_bank_name' => null,
|
||||
'payee_bank_code' => null,
|
||||
'payee_account_number' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the payment method is bank transfer.
|
||||
*/
|
||||
public function bankTransfer(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'payment_method' => 'bank_transfer',
|
||||
'payee_bank_name' => $this->faker->company() . ' Bank',
|
||||
'payee_bank_code' => $this->faker->numerify('###'),
|
||||
'payee_account_number' => $this->faker->numerify('##########'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
database/factories/UserFactory.php
Normal file
44
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
32
database/migrations/2014_10_12_000000_create_users_table.php
Normal file
32
database/migrations/2014_10_12_000000_create_users_table.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->string('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('document_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // 協會辦法, 法規, 會議記錄, 表格
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->string('icon')->nullable(); // emoji or FontAwesome class
|
||||
$table->integer('sort_order')->default(0);
|
||||
|
||||
// Default access level for documents in this category
|
||||
$table->enum('default_access_level', ['public', 'members', 'admin', 'board'])->default('members');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('document_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('document_category_id')->constrained()->onDelete('cascade');
|
||||
|
||||
// Document metadata
|
||||
$table->string('title');
|
||||
$table->string('document_number')->unique()->nullable(); // e.g., BYL-2024-001
|
||||
$table->text('description')->nullable();
|
||||
$table->uuid('public_uuid')->unique(); // For public sharing links
|
||||
|
||||
// Access control
|
||||
$table->enum('access_level', ['public', 'members', 'admin', 'board'])->default('members');
|
||||
|
||||
// Current version pointer (set after first version is created)
|
||||
$table->foreignId('current_version_id')->nullable()->constrained('document_versions')->onDelete('set null');
|
||||
|
||||
// Status
|
||||
$table->enum('status', ['active', 'archived'])->default('active');
|
||||
$table->timestamp('archived_at')->nullable();
|
||||
|
||||
// User tracking
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('last_updated_by_user_id')->nullable()->constrained('users')->onDelete('set null');
|
||||
|
||||
// Statistics
|
||||
$table->integer('view_count')->default(0);
|
||||
$table->integer('download_count')->default(0);
|
||||
$table->integer('version_count')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// Indexes
|
||||
$table->index('document_category_id');
|
||||
$table->index('access_level');
|
||||
$table->index('status');
|
||||
$table->index('public_uuid');
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('documents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('document_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('document_id')->constrained()->onDelete('cascade');
|
||||
|
||||
// Version information
|
||||
$table->string('version_number'); // 1.0, 1.1, 2.0, etc.
|
||||
$table->text('version_notes')->nullable(); // What changed in this version
|
||||
$table->boolean('is_current')->default(false); // Is this the current published version
|
||||
|
||||
// File information
|
||||
$table->string('file_path'); // storage/documents/...
|
||||
$table->string('original_filename');
|
||||
$table->string('mime_type');
|
||||
$table->unsignedBigInteger('file_size'); // in bytes
|
||||
$table->string('file_hash')->nullable(); // SHA-256 hash for integrity verification
|
||||
|
||||
// User tracking
|
||||
$table->foreignId('uploaded_by_user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamp('uploaded_at');
|
||||
|
||||
// Make version immutable after creation (no updated_at)
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('document_id');
|
||||
$table->index('version_number');
|
||||
$table->index('is_current');
|
||||
$table->index('uploaded_at');
|
||||
|
||||
// Unique constraint: only one current version per document
|
||||
$table->unique(['document_id', 'is_current'], 'unique_current_version');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('document_versions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('document_access_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('document_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('document_version_id')->nullable()->constrained()->onDelete('set null');
|
||||
|
||||
// Access information
|
||||
$table->enum('action', ['view', 'download']); // What action was performed
|
||||
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // null if anonymous/public access
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
|
||||
// Timestamps
|
||||
$table->timestamp('accessed_at');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('document_id');
|
||||
$table->index('user_id');
|
||||
$table->index('action');
|
||||
$table->index('accessed_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('document_access_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('full_name');
|
||||
$table->string('email')->index();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('national_id_encrypted')->nullable();
|
||||
$table->string('national_id_hash')->nullable()->index();
|
||||
$table->date('membership_started_at')->nullable();
|
||||
$table->date('membership_expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('members');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_payments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained()->cascadeOnDelete();
|
||||
$table->date('paid_at');
|
||||
$table->decimal('amount', 10, 2);
|
||||
$table->string('method')->nullable();
|
||||
$table->string('reference')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_payments');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('email');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$teams = config('permission.teams');
|
||||
$tableNames = config('permission.table_names');
|
||||
$columnNames = config('permission.column_names');
|
||||
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||
|
||||
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
// $table->engine('InnoDB');
|
||||
$table->bigIncrements('id'); // permission id
|
||||
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
|
||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||
// $table->engine('InnoDB');
|
||||
$table->bigIncrements('id'); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||
}
|
||||
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||
$table->timestamps();
|
||||
if ($teams || config('permission.testing')) {
|
||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||
} else {
|
||||
$table->unique(['name', 'guard_name']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->onDelete('cascade');
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
} else {
|
||||
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->onDelete('cascade');
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
} else {
|
||||
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
|
||||
app('cache')
|
||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||
->forget(config('permission.cache.key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$tableNames = config('permission.table_names');
|
||||
|
||||
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||
|
||||
Schema::drop($tableNames['role_has_permissions']);
|
||||
Schema::drop($tableNames['model_has_roles']);
|
||||
Schema::drop($tableNames['model_has_permissions']);
|
||||
Schema::drop($tableNames['roles']);
|
||||
Schema::drop($tableNames['permissions']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (! class_exists(\Spatie\Permission\Models\Role::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$roleClass = \Spatie\Permission\Models\Role::class;
|
||||
|
||||
$adminRole = $roleClass::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
|
||||
User::where('is_admin', true)->each(function (User $user) use ($adminRole) {
|
||||
$user->assignRole($adminRole);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->timestamp('last_expiry_reminder_sent_at')->nullable()->after('membership_expires_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropColumn('last_expiry_reminder_sent_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('audit_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('action');
|
||||
$table->string('auditable_type')->nullable();
|
||||
$table->unsignedBigInteger('auditable_id')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('audit_logs');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('finance_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('submitted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('title');
|
||||
$table->decimal('amount', 10, 2)->nullable();
|
||||
$table->string('status')->default('pending');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamp('submitted_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('finance_documents');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->string('address_line_1')->nullable()->after('phone');
|
||||
$table->string('address_line_2')->nullable()->after('address_line_1');
|
||||
$table->string('city')->nullable()->after('address_line_2');
|
||||
$table->string('postal_code')->nullable()->after('city');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropColumn(['address_line_1', 'address_line_2', 'city', 'postal_code']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->string('description')->nullable()->after('guard_name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->dropColumn('description');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->string('emergency_contact_name')->nullable()->after('postal_code');
|
||||
$table->string('emergency_contact_phone')->nullable()->after('emergency_contact_name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropColumn(['emergency_contact_name', 'emergency_contact_phone']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('profile_photo_path')->nullable()->after('is_admin');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('profile_photo_path');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
// File attachment
|
||||
$table->string('attachment_path')->nullable()->after('description');
|
||||
|
||||
// Cashier approval
|
||||
$table->foreignId('approved_by_cashier_id')->nullable()->constrained('users')->nullOnDelete()->after('attachment_path');
|
||||
$table->timestamp('cashier_approved_at')->nullable()->after('approved_by_cashier_id');
|
||||
|
||||
// Accountant approval
|
||||
$table->foreignId('approved_by_accountant_id')->nullable()->constrained('users')->nullOnDelete()->after('cashier_approved_at');
|
||||
$table->timestamp('accountant_approved_at')->nullable()->after('approved_by_accountant_id');
|
||||
|
||||
// Chair approval
|
||||
$table->foreignId('approved_by_chair_id')->nullable()->constrained('users')->nullOnDelete()->after('accountant_approved_at');
|
||||
$table->timestamp('chair_approved_at')->nullable()->after('approved_by_chair_id');
|
||||
|
||||
// Rejection fields
|
||||
$table->foreignId('rejected_by_user_id')->nullable()->constrained('users')->nullOnDelete()->after('chair_approved_at');
|
||||
$table->timestamp('rejected_at')->nullable()->after('rejected_by_user_id');
|
||||
$table->text('rejection_reason')->nullable()->after('rejected_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
$table->dropForeign(['approved_by_cashier_id']);
|
||||
$table->dropForeign(['approved_by_accountant_id']);
|
||||
$table->dropForeign(['approved_by_chair_id']);
|
||||
$table->dropForeign(['rejected_by_user_id']);
|
||||
|
||||
$table->dropColumn([
|
||||
'attachment_path',
|
||||
'approved_by_cashier_id',
|
||||
'cashier_approved_at',
|
||||
'approved_by_accountant_id',
|
||||
'accountant_approved_at',
|
||||
'approved_by_chair_id',
|
||||
'chair_approved_at',
|
||||
'rejected_by_user_id',
|
||||
'rejected_at',
|
||||
'rejection_reason',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('chart_of_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('account_code', 10)->unique()->comment('Account code (e.g., 4101)');
|
||||
$table->string('account_name_zh')->comment('Chinese account name');
|
||||
$table->string('account_name_en')->nullable()->comment('English account name');
|
||||
$table->enum('account_type', ['asset', 'liability', 'net_asset', 'income', 'expense'])->comment('Account type');
|
||||
$table->string('category')->nullable()->comment('Detailed category');
|
||||
$table->foreignId('parent_account_id')->nullable()->constrained('chart_of_accounts')->nullOnDelete()->comment('Parent account for hierarchical structure');
|
||||
$table->boolean('is_active')->default(true)->comment('Active status');
|
||||
$table->integer('display_order')->default(0)->comment('Display order');
|
||||
$table->text('description')->nullable()->comment('Account description');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('account_type');
|
||||
$table->index('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('chart_of_accounts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('budget_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('budget_id')->constrained()->cascadeOnDelete()->comment('Budget reference');
|
||||
$table->foreignId('chart_of_account_id')->constrained()->cascadeOnDelete()->comment('Chart of account reference');
|
||||
$table->decimal('budgeted_amount', 15, 2)->default(0)->comment('Budgeted amount');
|
||||
$table->decimal('actual_amount', 15, 2)->default(0)->comment('Actual amount (calculated)');
|
||||
$table->text('notes')->nullable()->comment('Item notes');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['budget_id', 'chart_of_account_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('budget_items');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('budgets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('fiscal_year')->comment('Fiscal year (e.g., 2025)');
|
||||
$table->string('name')->comment('Budget name');
|
||||
$table->enum('period_type', ['annual', 'quarterly', 'monthly'])->default('annual')->comment('Budget period type');
|
||||
$table->date('period_start')->comment('Period start date');
|
||||
$table->date('period_end')->comment('Period end date');
|
||||
$table->enum('status', ['draft', 'submitted', 'approved', 'active', 'closed'])->default('draft')->comment('Budget status');
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete()->comment('Created by user');
|
||||
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('Approved by user');
|
||||
$table->timestamp('approved_at')->nullable()->comment('Approval timestamp');
|
||||
$table->text('notes')->nullable()->comment('Budget notes');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('fiscal_year');
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('budgets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('budget_item_id')->nullable()->constrained()->nullOnDelete()->comment('Budget item reference');
|
||||
$table->foreignId('chart_of_account_id')->constrained()->cascadeOnDelete()->comment('Chart of account reference');
|
||||
$table->date('transaction_date')->comment('Transaction date');
|
||||
$table->decimal('amount', 15, 2)->comment('Transaction amount');
|
||||
$table->enum('transaction_type', ['income', 'expense'])->comment('Transaction type');
|
||||
$table->string('description')->comment('Transaction description');
|
||||
$table->string('reference_number')->nullable()->comment('Reference/receipt number');
|
||||
$table->foreignId('finance_document_id')->nullable()->constrained()->nullOnDelete()->comment('Related finance document');
|
||||
$table->foreignId('membership_payment_id')->nullable()->constrained()->nullOnDelete()->comment('Related membership payment');
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete()->comment('Created by user');
|
||||
$table->text('notes')->nullable()->comment('Additional notes');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('transaction_date');
|
||||
$table->index('transaction_type');
|
||||
$table->index(['budget_item_id', 'transaction_date']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('transactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('financial_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('report_type', ['revenue_expenditure', 'balance_sheet', 'property_inventory', 'internal_management'])->comment('Report type');
|
||||
$table->integer('fiscal_year')->comment('Fiscal year');
|
||||
$table->date('period_start')->comment('Period start date');
|
||||
$table->date('period_end')->comment('Period end date');
|
||||
$table->enum('status', ['draft', 'finalized', 'approved', 'submitted'])->default('draft')->comment('Report status');
|
||||
$table->foreignId('budget_id')->nullable()->constrained()->nullOnDelete()->comment('Related budget');
|
||||
$table->foreignId('generated_by_user_id')->constrained('users')->cascadeOnDelete()->comment('Generated by user');
|
||||
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('Approved by user');
|
||||
$table->timestamp('approved_at')->nullable()->comment('Approval timestamp');
|
||||
$table->string('file_path')->nullable()->comment('PDF/Excel file path');
|
||||
$table->text('notes')->nullable()->comment('Report notes');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['report_type', 'fiscal_year']);
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('financial_reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issues', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('issue_number')->unique()->comment('Auto-generated issue number (e.g., ISS-2025-001)');
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
|
||||
// Issue categorization
|
||||
$table->enum('issue_type', ['work_item', 'project_task', 'maintenance', 'member_request'])
|
||||
->default('work_item')
|
||||
->comment('Type of issue');
|
||||
$table->enum('status', ['new', 'assigned', 'in_progress', 'review', 'closed'])
|
||||
->default('new')
|
||||
->comment('Current workflow status');
|
||||
$table->enum('priority', ['low', 'medium', 'high', 'urgent'])
|
||||
->default('medium')
|
||||
->comment('Priority level');
|
||||
|
||||
// User relationships
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete()->comment('User who created the issue');
|
||||
$table->foreignId('assigned_to_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('User assigned to work on this');
|
||||
$table->foreignId('reviewer_id')->nullable()->constrained('users')->nullOnDelete()->comment('User assigned to review');
|
||||
|
||||
// Related entities
|
||||
$table->foreignId('member_id')->nullable()->constrained('members')->nullOnDelete()->comment('Related member (for member requests)');
|
||||
$table->foreignId('parent_issue_id')->nullable()->constrained('issues')->nullOnDelete()->comment('Parent issue for sub-tasks');
|
||||
|
||||
// Dates and time tracking
|
||||
$table->date('due_date')->nullable()->comment('Deadline for completion');
|
||||
$table->timestamp('closed_at')->nullable()->comment('When issue was closed');
|
||||
$table->decimal('estimated_hours', 8, 2)->nullable()->comment('Estimated time to complete');
|
||||
$table->decimal('actual_hours', 8, 2)->default(0)->comment('Actual time spent (sum of time logs)');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// Indexes for common queries
|
||||
$table->index('issue_type');
|
||||
$table->index('status');
|
||||
$table->index('priority');
|
||||
$table->index('assigned_to_user_id');
|
||||
$table->index('created_by_user_id');
|
||||
$table->index('due_date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issues');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_comments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->text('comment_text');
|
||||
$table->boolean('is_internal')->default(false)->comment('Hide from members if true');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('issue_id');
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_comments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete()->comment('User who uploaded');
|
||||
$table->string('file_name');
|
||||
$table->string('file_path');
|
||||
$table->unsignedBigInteger('file_size')->comment('File size in bytes');
|
||||
$table->string('mime_type');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('issue_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('custom_field_values', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('custom_field_id')->constrained('custom_fields')->cascadeOnDelete();
|
||||
$table->morphs('customizable'); // customizable_type and customizable_id (for issues)
|
||||
$table->json('value')->comment('Stored value (JSON for flexibility)');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('custom_field_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_field_values');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('custom_fields', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->enum('field_type', ['text', 'number', 'date', 'select'])->comment('Data type');
|
||||
$table->json('options')->nullable()->comment('Options for select type fields');
|
||||
$table->json('applies_to_issue_types')->comment('Which issue types can use this field');
|
||||
$table->boolean('is_required')->default(false);
|
||||
$table->integer('display_order')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_fields');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_label_pivot', function (Blueprint $table) {
|
||||
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->foreignId('issue_label_id')->constrained('issue_labels')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
// Composite primary key
|
||||
$table->primary(['issue_id', 'issue_label_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_label_pivot');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_labels', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('color', 7)->default('#6B7280')->comment('Hex color code');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_labels');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_relationships', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->foreignId('related_issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->enum('relationship_type', ['blocks', 'blocked_by', 'related_to', 'duplicate_of'])
|
||||
->comment('Type of relationship');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('issue_id');
|
||||
$table->index('related_issue_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_relationships');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_time_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->decimal('hours', 8, 2)->comment('Hours worked');
|
||||
$table->text('description')->nullable()->comment('What was done');
|
||||
$table->timestamp('logged_at')->comment('When the work was performed');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('issue_id');
|
||||
$table->index('user_id');
|
||||
$table->index('logged_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_time_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('issue_watchers', function (Blueprint $table) {
|
||||
$table->foreignId('issue_id')->constrained('issues')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
// Composite primary key to prevent duplicate watchers
|
||||
$table->primary(['issue_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('issue_watchers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('membership_payments', function (Blueprint $table) {
|
||||
// Payment verification workflow status
|
||||
$table->enum('status', ['pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected'])
|
||||
->default('pending')
|
||||
->after('reference');
|
||||
|
||||
// Payment method
|
||||
$table->enum('payment_method', ['bank_transfer', 'convenience_store', 'cash', 'credit_card'])
|
||||
->nullable()
|
||||
->after('status');
|
||||
|
||||
// Receipt file upload
|
||||
$table->string('receipt_path')->nullable()->after('payment_method');
|
||||
|
||||
// Submitted by (member self-submission)
|
||||
$table->foreignId('submitted_by_user_id')->nullable()->after('receipt_path')
|
||||
->constrained('users')->nullOnDelete();
|
||||
|
||||
// Cashier verification (Tier 1)
|
||||
$table->foreignId('verified_by_cashier_id')->nullable()->after('submitted_by_user_id')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('cashier_verified_at')->nullable()->after('verified_by_cashier_id');
|
||||
|
||||
// Accountant verification (Tier 2)
|
||||
$table->foreignId('verified_by_accountant_id')->nullable()->after('cashier_verified_at')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('accountant_verified_at')->nullable()->after('verified_by_accountant_id');
|
||||
|
||||
// Chair verification (Tier 3)
|
||||
$table->foreignId('verified_by_chair_id')->nullable()->after('accountant_verified_at')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('chair_verified_at')->nullable()->after('verified_by_chair_id');
|
||||
|
||||
// Rejection tracking
|
||||
$table->foreignId('rejected_by_user_id')->nullable()->after('chair_verified_at')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('rejected_at')->nullable()->after('rejected_by_user_id');
|
||||
$table->text('rejection_reason')->nullable()->after('rejected_at');
|
||||
|
||||
// Admin notes
|
||||
$table->text('notes')->nullable()->after('rejection_reason');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('membership_payments', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'status',
|
||||
'payment_method',
|
||||
'receipt_path',
|
||||
'submitted_by_user_id',
|
||||
'verified_by_cashier_id',
|
||||
'cashier_verified_at',
|
||||
'verified_by_accountant_id',
|
||||
'accountant_verified_at',
|
||||
'verified_by_chair_id',
|
||||
'chair_verified_at',
|
||||
'rejected_by_user_id',
|
||||
'rejected_at',
|
||||
'rejection_reason',
|
||||
'notes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
// Membership status - distinguishes paid vs unpaid members
|
||||
$table->enum('membership_status', ['pending', 'active', 'expired', 'suspended'])
|
||||
->default('pending')
|
||||
->after('membership_expires_at')
|
||||
->comment('Payment verification status: pending (not paid), active (paid & activated), expired, suspended');
|
||||
|
||||
// Membership type - for different membership tiers
|
||||
$table->enum('membership_type', ['regular', 'honorary', 'lifetime', 'student'])
|
||||
->default('regular')
|
||||
->after('membership_status')
|
||||
->comment('Type of membership: regular (annual fee), honorary (no fee), lifetime (one-time), student (discounted)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropColumn(['membership_status', 'membership_type']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* The unique constraint on (document_id, is_current) was too restrictive.
|
||||
* It prevented having multiple versions with is_current = false.
|
||||
* We only need to ensure ONE version has is_current = true, which is
|
||||
* enforced in the application logic.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('document_versions', function (Blueprint $table) {
|
||||
$table->dropUnique('unique_current_version');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('document_versions', function (Blueprint $table) {
|
||||
$table->unique(['document_id', 'is_current'], 'unique_current_version');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Create tags table
|
||||
Schema::create('document_tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('color')->default('#6366f1'); // Indigo color
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Create pivot table for document-tag relationship
|
||||
Schema::create('document_document_tag', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('document_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('document_tag_id')->constrained()->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['document_id', 'document_tag_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('document_document_tag');
|
||||
Schema::dropIfExists('document_tags');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documents', function (Blueprint $table) {
|
||||
$table->date('expires_at')->nullable()->after('status');
|
||||
$table->boolean('auto_archive_on_expiry')->default(false)->after('expires_at');
|
||||
$table->text('expiry_notice')->nullable()->after('auto_archive_on_expiry');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documents', function (Blueprint $table) {
|
||||
$table->dropColumn(['expires_at', 'auto_archive_on_expiry', 'expiry_notice']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('system_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique()->comment('Setting key (e.g., documents.qr_enabled)');
|
||||
$table->text('value')->nullable()->comment('Setting value (JSON for complex values)');
|
||||
$table->enum('type', ['string', 'integer', 'boolean', 'json', 'array'])->default('string')->comment('Value type for casting');
|
||||
$table->string('group')->nullable()->index()->comment('Settings group (e.g., documents, security, notifications)');
|
||||
$table->text('description')->nullable()->comment('Human-readable description of this setting');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('system_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
// 申請類型和金額分級
|
||||
$table->enum('request_type', ['expense_reimbursement', 'advance_payment', 'purchase_request', 'petty_cash'])
|
||||
->default('expense_reimbursement')
|
||||
->after('status')
|
||||
->comment('申請類型:費用報銷/預支借款/採購申請/零用金');
|
||||
|
||||
$table->enum('amount_tier', ['small', 'medium', 'large'])
|
||||
->nullable()
|
||||
->after('request_type')
|
||||
->comment('金額層級:小額(<5000)/中額(5000-50000)/大額(>50000)');
|
||||
|
||||
// 會計科目分配(會計審核時填寫)
|
||||
$table->foreignId('chart_of_account_id')->nullable()->after('amount_tier')->constrained('chart_of_accounts')->nullOnDelete();
|
||||
$table->foreignId('budget_item_id')->nullable()->after('chart_of_account_id')->constrained('budget_items')->nullOnDelete();
|
||||
|
||||
// 理監事會議核准(大額)
|
||||
$table->boolean('requires_board_meeting')->default(false)->after('chair_approved_at');
|
||||
$table->date('board_meeting_date')->nullable()->after('requires_board_meeting');
|
||||
$table->text('board_meeting_decision')->nullable()->after('board_meeting_date');
|
||||
$table->foreignId('approved_by_board_meeting_id')->nullable()->after('board_meeting_decision')->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('board_meeting_approved_at')->nullable()->after('approved_by_board_meeting_id');
|
||||
|
||||
// 付款單製作(會計)
|
||||
$table->foreignId('payment_order_created_by_accountant_id')->nullable()->after('board_meeting_approved_at')->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('payment_order_created_at')->nullable()->after('payment_order_created_by_accountant_id');
|
||||
$table->enum('payment_method', ['bank_transfer', 'check', 'cash'])->nullable()->after('payment_order_created_at');
|
||||
$table->string('payee_name', 100)->nullable()->after('payment_method');
|
||||
$table->string('payee_bank_code', 10)->nullable()->after('payee_name');
|
||||
$table->string('payee_account_number', 30)->nullable()->after('payee_bank_code');
|
||||
$table->text('payment_notes')->nullable()->after('payee_account_number');
|
||||
|
||||
// 出納覆核付款單
|
||||
$table->foreignId('payment_verified_by_cashier_id')->nullable()->after('payment_notes')->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('payment_verified_at')->nullable()->after('payment_verified_by_cashier_id');
|
||||
$table->text('payment_verification_notes')->nullable()->after('payment_verified_at');
|
||||
|
||||
// 實際付款執行
|
||||
$table->foreignId('payment_executed_by_cashier_id')->nullable()->after('payment_verification_notes')->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('payment_executed_at')->nullable()->after('payment_executed_by_cashier_id');
|
||||
$table->string('payment_transaction_id', 50)->nullable()->after('payment_executed_at')->comment('銀行交易編號');
|
||||
$table->string('payment_receipt_path')->nullable()->after('payment_transaction_id')->comment('付款憑證路徑');
|
||||
$table->decimal('actual_payment_amount', 10, 2)->nullable()->after('payment_receipt_path')->comment('實付金額');
|
||||
|
||||
// 記帳階段 (外鍵稍後加上,因為相關表還不存在)
|
||||
$table->unsignedBigInteger('cashier_ledger_entry_id')->nullable()->after('actual_payment_amount');
|
||||
$table->timestamp('cashier_recorded_at')->nullable()->after('cashier_ledger_entry_id');
|
||||
$table->unsignedBigInteger('accounting_transaction_id')->nullable()->after('cashier_recorded_at');
|
||||
$table->timestamp('accountant_recorded_at')->nullable()->after('accounting_transaction_id');
|
||||
|
||||
// 月底核對
|
||||
$table->enum('reconciliation_status', ['pending', 'matched', 'discrepancy', 'resolved'])->default('pending')->after('accountant_recorded_at');
|
||||
$table->text('reconciliation_notes')->nullable()->after('reconciliation_status');
|
||||
$table->timestamp('reconciled_at')->nullable()->after('reconciliation_notes');
|
||||
$table->foreignId('reconciled_by_user_id')->nullable()->after('reconciled_at')->constrained('users')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
// 移除外鍵約束
|
||||
$table->dropForeign(['chart_of_account_id']);
|
||||
$table->dropForeign(['budget_item_id']);
|
||||
$table->dropForeign(['approved_by_board_meeting_id']);
|
||||
$table->dropForeign(['payment_order_created_by_accountant_id']);
|
||||
$table->dropForeign(['payment_verified_by_cashier_id']);
|
||||
$table->dropForeign(['payment_executed_by_cashier_id']);
|
||||
$table->dropForeign(['reconciled_by_user_id']);
|
||||
|
||||
// 移除欄位
|
||||
$table->dropColumn([
|
||||
'request_type',
|
||||
'amount_tier',
|
||||
'chart_of_account_id',
|
||||
'budget_item_id',
|
||||
'requires_board_meeting',
|
||||
'board_meeting_date',
|
||||
'board_meeting_decision',
|
||||
'approved_by_board_meeting_id',
|
||||
'board_meeting_approved_at',
|
||||
'payment_order_created_by_accountant_id',
|
||||
'payment_order_created_at',
|
||||
'payment_method',
|
||||
'payee_name',
|
||||
'payee_bank_code',
|
||||
'payee_account_number',
|
||||
'payment_notes',
|
||||
'payment_verified_by_cashier_id',
|
||||
'payment_verified_at',
|
||||
'payment_verification_notes',
|
||||
'payment_executed_by_cashier_id',
|
||||
'payment_executed_at',
|
||||
'payment_transaction_id',
|
||||
'payment_receipt_path',
|
||||
'actual_payment_amount',
|
||||
'cashier_ledger_entry_id',
|
||||
'cashier_recorded_at',
|
||||
'accounting_transaction_id',
|
||||
'accountant_recorded_at',
|
||||
'reconciliation_status',
|
||||
'reconciliation_notes',
|
||||
'reconciled_at',
|
||||
'reconciled_by_user_id',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payment_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('finance_document_id')->constrained('finance_documents')->cascadeOnDelete();
|
||||
|
||||
// 付款資訊
|
||||
$table->string('payee_name', 100)->comment('收款人姓名');
|
||||
$table->string('payee_bank_code', 10)->nullable()->comment('銀行代碼');
|
||||
$table->string('payee_account_number', 30)->nullable()->comment('銀行帳號');
|
||||
$table->string('payee_bank_name', 100)->nullable()->comment('銀行名稱');
|
||||
$table->decimal('payment_amount', 10, 2)->comment('付款金額');
|
||||
$table->enum('payment_method', ['bank_transfer', 'check', 'cash'])->comment('付款方式');
|
||||
|
||||
// 會計製單
|
||||
$table->foreignId('created_by_accountant_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('payment_order_number', 50)->unique()->comment('付款單號');
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
// 出納覆核
|
||||
$table->foreignId('verified_by_cashier_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('verified_at')->nullable();
|
||||
$table->enum('verification_status', ['pending', 'approved', 'rejected'])->default('pending');
|
||||
$table->text('verification_notes')->nullable();
|
||||
|
||||
// 執行付款
|
||||
$table->foreignId('executed_by_cashier_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('executed_at')->nullable();
|
||||
$table->enum('execution_status', ['pending', 'completed', 'failed'])->default('pending');
|
||||
$table->string('transaction_reference', 100)->nullable()->comment('交易參考號');
|
||||
|
||||
// 憑證
|
||||
$table->string('payment_receipt_path')->nullable()->comment('付款憑證路徑');
|
||||
|
||||
$table->enum('status', ['draft', 'pending_verification', 'verified', 'executed', 'cancelled'])->default('draft');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('finance_document_id');
|
||||
$table->index('status');
|
||||
$table->index('verification_status');
|
||||
$table->index('execution_status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payment_orders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cashier_ledger_entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('finance_document_id')->nullable()->constrained('finance_documents')->cascadeOnDelete();
|
||||
$table->date('entry_date')->comment('記帳日期');
|
||||
$table->enum('entry_type', ['receipt', 'payment'])->comment('類型:收入/支出');
|
||||
|
||||
// 付款資訊
|
||||
$table->enum('payment_method', ['bank_transfer', 'check', 'cash'])->comment('付款方式');
|
||||
$table->string('bank_account', 100)->nullable()->comment('使用的銀行帳戶');
|
||||
$table->decimal('amount', 10, 2)->comment('金額');
|
||||
|
||||
// 餘額追蹤
|
||||
$table->decimal('balance_before', 10, 2)->comment('交易前餘額');
|
||||
$table->decimal('balance_after', 10, 2)->comment('交易後餘額');
|
||||
|
||||
// 憑證資訊
|
||||
$table->string('receipt_number', 50)->nullable()->comment('收據/憑證編號');
|
||||
$table->string('transaction_reference', 100)->nullable()->comment('交易參考號');
|
||||
|
||||
// 記錄人員
|
||||
$table->foreignId('recorded_by_cashier_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->timestamp('recorded_at')->useCurrent();
|
||||
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('finance_document_id');
|
||||
$table->index('entry_date');
|
||||
$table->index('entry_type');
|
||||
$table->index('recorded_by_cashier_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cashier_ledger_entries');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bank_reconciliations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('reconciliation_month')->comment('調節月份');
|
||||
|
||||
// 銀行對帳單
|
||||
$table->decimal('bank_statement_balance', 10, 2)->comment('銀行對帳單餘額');
|
||||
$table->date('bank_statement_date')->comment('對帳單日期');
|
||||
$table->string('bank_statement_file_path')->nullable()->comment('對帳單檔案');
|
||||
|
||||
// 系統帳面
|
||||
$table->decimal('system_book_balance', 10, 2)->comment('系統帳面餘額');
|
||||
|
||||
// 未達帳項(JSON 格式)
|
||||
$table->json('outstanding_checks')->nullable()->comment('未兌現支票');
|
||||
$table->json('deposits_in_transit')->nullable()->comment('在途存款');
|
||||
$table->json('bank_charges')->nullable()->comment('銀行手續費');
|
||||
|
||||
// 調節結果
|
||||
$table->decimal('adjusted_balance', 10, 2)->comment('調整後餘額');
|
||||
$table->decimal('discrepancy_amount', 10, 2)->default(0)->comment('差異金額');
|
||||
$table->enum('reconciliation_status', ['pending', 'completed', 'discrepancy'])->default('pending');
|
||||
|
||||
// 執行人員
|
||||
$table->foreignId('prepared_by_cashier_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('reviewed_by_accountant_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('approved_by_manager_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
$table->timestamp('prepared_at')->useCurrent();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('reconciliation_month');
|
||||
$table->index('reconciliation_status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bank_reconciliations');
|
||||
}
|
||||
};
|
||||
92
database/seeders/AdvancedPermissionsSeeder.php
Normal file
92
database/seeders/AdvancedPermissionsSeeder.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class AdvancedPermissionsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Create advanced document feature permissions
|
||||
$permissions = [
|
||||
[
|
||||
'name' => 'manage_system_settings',
|
||||
'description' => 'Access and modify system settings pages'
|
||||
],
|
||||
[
|
||||
'name' => 'use_bulk_import',
|
||||
'description' => 'Use document bulk import feature'
|
||||
],
|
||||
[
|
||||
'name' => 'use_qr_codes',
|
||||
'description' => 'Generate QR codes for documents'
|
||||
],
|
||||
[
|
||||
'name' => 'view_document_statistics',
|
||||
'description' => 'Access document statistics dashboard'
|
||||
],
|
||||
[
|
||||
'name' => 'manage_document_tags',
|
||||
'description' => 'Create, edit, and delete document tags'
|
||||
],
|
||||
[
|
||||
'name' => 'manage_document_expiration',
|
||||
'description' => 'Set expiration dates and configure auto-archive rules'
|
||||
],
|
||||
[
|
||||
'name' => 'export_documents',
|
||||
'description' => 'Export document lists and reports'
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($permissions as $permissionData) {
|
||||
Permission::firstOrCreate(
|
||||
['name' => $permissionData['name']],
|
||||
['guard_name' => 'web']
|
||||
);
|
||||
}
|
||||
|
||||
// Assign all advanced permissions to 'admin' role
|
||||
$adminRole = Role::where('name', 'admin')->first();
|
||||
|
||||
if ($adminRole) {
|
||||
foreach ($permissions as $permissionData) {
|
||||
$permission = Permission::where('name', $permissionData['name'])->first();
|
||||
if ($permission && !$adminRole->hasPermissionTo($permission)) {
|
||||
$adminRole->givePermissionTo($permission);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('Advanced permissions assigned to admin role');
|
||||
}
|
||||
|
||||
// Optionally assign some permissions to 'staff' role
|
||||
$staffRole = Role::where('name', 'staff')->first();
|
||||
|
||||
if ($staffRole) {
|
||||
$staffPermissions = [
|
||||
'use_qr_codes',
|
||||
'view_document_statistics',
|
||||
'export_documents',
|
||||
];
|
||||
|
||||
foreach ($staffPermissions as $permissionName) {
|
||||
$permission = Permission::where('name', $permissionName)->first();
|
||||
if ($permission && !$staffRole->hasPermissionTo($permission)) {
|
||||
$staffRole->givePermissionTo($permission);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('Selected permissions assigned to staff role');
|
||||
}
|
||||
|
||||
$this->command->info('Advanced permissions seeded successfully');
|
||||
}
|
||||
}
|
||||
490
database/seeders/ChartOfAccountSeeder.php
Normal file
490
database/seeders/ChartOfAccountSeeder.php
Normal file
@@ -0,0 +1,490 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\ChartOfAccount;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ChartOfAccountSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* Taiwan nonprofit standard chart of accounts (會計科目表)
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$accounts = [
|
||||
// Assets (資產) - 1xxx
|
||||
[
|
||||
'account_code' => '1101',
|
||||
'account_name_zh' => '現金',
|
||||
'account_name_en' => 'Cash',
|
||||
'account_type' => 'asset',
|
||||
'category' => '流動資產',
|
||||
'display_order' => 10,
|
||||
],
|
||||
[
|
||||
'account_code' => '1102',
|
||||
'account_name_zh' => '零用金',
|
||||
'account_name_en' => 'Petty Cash',
|
||||
'account_type' => 'asset',
|
||||
'category' => '流動資產',
|
||||
'display_order' => 20,
|
||||
],
|
||||
[
|
||||
'account_code' => '1201',
|
||||
'account_name_zh' => '銀行存款',
|
||||
'account_name_en' => 'Bank Deposits',
|
||||
'account_type' => 'asset',
|
||||
'category' => '流動資產',
|
||||
'display_order' => 30,
|
||||
],
|
||||
[
|
||||
'account_code' => '1301',
|
||||
'account_name_zh' => '應收帳款',
|
||||
'account_name_en' => 'Accounts Receivable',
|
||||
'account_type' => 'asset',
|
||||
'category' => '流動資產',
|
||||
'display_order' => 40,
|
||||
],
|
||||
[
|
||||
'account_code' => '1302',
|
||||
'account_name_zh' => '其他應收款',
|
||||
'account_name_en' => 'Other Receivables',
|
||||
'account_type' => 'asset',
|
||||
'category' => '流動資產',
|
||||
'display_order' => 50,
|
||||
],
|
||||
[
|
||||
'account_code' => '1401',
|
||||
'account_name_zh' => '土地',
|
||||
'account_name_en' => 'Land',
|
||||
'account_type' => 'asset',
|
||||
'category' => '固定資產',
|
||||
'display_order' => 60,
|
||||
],
|
||||
[
|
||||
'account_code' => '1402',
|
||||
'account_name_zh' => '房屋及建築',
|
||||
'account_name_en' => 'Buildings',
|
||||
'account_type' => 'asset',
|
||||
'category' => '固定資產',
|
||||
'display_order' => 70,
|
||||
],
|
||||
[
|
||||
'account_code' => '1403',
|
||||
'account_name_zh' => '機器設備',
|
||||
'account_name_en' => 'Machinery & Equipment',
|
||||
'account_type' => 'asset',
|
||||
'category' => '固定資產',
|
||||
'display_order' => 80,
|
||||
],
|
||||
[
|
||||
'account_code' => '1404',
|
||||
'account_name_zh' => '辦公設備',
|
||||
'account_name_en' => 'Office Equipment',
|
||||
'account_type' => 'asset',
|
||||
'category' => '固定資產',
|
||||
'display_order' => 90,
|
||||
],
|
||||
[
|
||||
'account_code' => '1405',
|
||||
'account_name_zh' => '電腦設備',
|
||||
'account_name_en' => 'Computer Equipment',
|
||||
'account_type' => 'asset',
|
||||
'category' => '固定資產',
|
||||
'display_order' => 100,
|
||||
],
|
||||
[
|
||||
'account_code' => '1501',
|
||||
'account_name_zh' => '存出保證金',
|
||||
'account_name_en' => 'Guarantee Deposits Paid',
|
||||
'account_type' => 'asset',
|
||||
'category' => '其他資產',
|
||||
'display_order' => 110,
|
||||
],
|
||||
|
||||
// Liabilities (負債) - 2xxx
|
||||
[
|
||||
'account_code' => '2101',
|
||||
'account_name_zh' => '應付帳款',
|
||||
'account_name_en' => 'Accounts Payable',
|
||||
'account_type' => 'liability',
|
||||
'category' => '流動負債',
|
||||
'display_order' => 200,
|
||||
],
|
||||
[
|
||||
'account_code' => '2102',
|
||||
'account_name_zh' => '應付薪資',
|
||||
'account_name_en' => 'Salaries Payable',
|
||||
'account_type' => 'liability',
|
||||
'category' => '流動負債',
|
||||
'display_order' => 210,
|
||||
],
|
||||
[
|
||||
'account_code' => '2103',
|
||||
'account_name_zh' => '應付費用',
|
||||
'account_name_en' => 'Accrued Expenses',
|
||||
'account_type' => 'liability',
|
||||
'category' => '流動負債',
|
||||
'display_order' => 220,
|
||||
],
|
||||
[
|
||||
'account_code' => '2104',
|
||||
'account_name_zh' => '代收款',
|
||||
'account_name_en' => 'Collections for Others',
|
||||
'account_type' => 'liability',
|
||||
'category' => '流動負債',
|
||||
'display_order' => 230,
|
||||
],
|
||||
[
|
||||
'account_code' => '2201',
|
||||
'account_name_zh' => '長期借款',
|
||||
'account_name_en' => 'Long-term Loans',
|
||||
'account_type' => 'liability',
|
||||
'category' => '長期負債',
|
||||
'display_order' => 240,
|
||||
],
|
||||
|
||||
// Net Assets/Fund Balance (淨資產/基金) - 3xxx
|
||||
[
|
||||
'account_code' => '3101',
|
||||
'account_name_zh' => '累積餘絀',
|
||||
'account_name_en' => 'Accumulated Surplus/Deficit',
|
||||
'account_type' => 'net_asset',
|
||||
'category' => '淨資產',
|
||||
'display_order' => 300,
|
||||
],
|
||||
[
|
||||
'account_code' => '3102',
|
||||
'account_name_zh' => '本期餘絀',
|
||||
'account_name_en' => 'Current Period Surplus/Deficit',
|
||||
'account_type' => 'net_asset',
|
||||
'category' => '淨資產',
|
||||
'display_order' => 310,
|
||||
],
|
||||
[
|
||||
'account_code' => '3201',
|
||||
'account_name_zh' => '基金',
|
||||
'account_name_en' => 'Fund Balance',
|
||||
'account_type' => 'net_asset',
|
||||
'category' => '基金',
|
||||
'display_order' => 320,
|
||||
],
|
||||
|
||||
// Income (收入) - 4xxx
|
||||
[
|
||||
'account_code' => '4101',
|
||||
'account_name_zh' => '會費收入',
|
||||
'account_name_en' => 'Membership Dues',
|
||||
'account_type' => 'income',
|
||||
'category' => '會費收入',
|
||||
'display_order' => 400,
|
||||
'description' => '會員繳交之常年會費',
|
||||
],
|
||||
[
|
||||
'account_code' => '4102',
|
||||
'account_name_zh' => '入會費收入',
|
||||
'account_name_en' => 'Entrance Fees',
|
||||
'account_type' => 'income',
|
||||
'category' => '會費收入',
|
||||
'display_order' => 410,
|
||||
'description' => '新會員入會費',
|
||||
],
|
||||
[
|
||||
'account_code' => '4201',
|
||||
'account_name_zh' => '捐贈收入',
|
||||
'account_name_en' => 'Donation Income',
|
||||
'account_type' => 'income',
|
||||
'category' => '捐贈收入',
|
||||
'display_order' => 420,
|
||||
'description' => '個人或團體捐贈',
|
||||
],
|
||||
[
|
||||
'account_code' => '4202',
|
||||
'account_name_zh' => '企業捐贈收入',
|
||||
'account_name_en' => 'Corporate Donations',
|
||||
'account_type' => 'income',
|
||||
'category' => '捐贈收入',
|
||||
'display_order' => 430,
|
||||
'description' => '企業捐贈',
|
||||
],
|
||||
[
|
||||
'account_code' => '4301',
|
||||
'account_name_zh' => '政府補助收入',
|
||||
'account_name_en' => 'Government Grants',
|
||||
'account_type' => 'income',
|
||||
'category' => '補助收入',
|
||||
'display_order' => 440,
|
||||
'description' => '政府機關補助款',
|
||||
],
|
||||
[
|
||||
'account_code' => '4302',
|
||||
'account_name_zh' => '計畫補助收入',
|
||||
'account_name_en' => 'Project Grants',
|
||||
'account_type' => 'income',
|
||||
'category' => '補助收入',
|
||||
'display_order' => 450,
|
||||
'description' => '專案計畫補助',
|
||||
],
|
||||
[
|
||||
'account_code' => '4401',
|
||||
'account_name_zh' => '利息收入',
|
||||
'account_name_en' => 'Interest Income',
|
||||
'account_type' => 'income',
|
||||
'category' => '其他收入',
|
||||
'display_order' => 460,
|
||||
],
|
||||
[
|
||||
'account_code' => '4402',
|
||||
'account_name_zh' => '活動報名費收入',
|
||||
'account_name_en' => 'Activity Registration Fees',
|
||||
'account_type' => 'income',
|
||||
'category' => '其他收入',
|
||||
'display_order' => 470,
|
||||
'description' => '各項活動報名費',
|
||||
],
|
||||
[
|
||||
'account_code' => '4901',
|
||||
'account_name_zh' => '雜項收入',
|
||||
'account_name_en' => 'Miscellaneous Income',
|
||||
'account_type' => 'income',
|
||||
'category' => '其他收入',
|
||||
'display_order' => 480,
|
||||
],
|
||||
|
||||
// Expenses (支出) - 5xxx
|
||||
// Personnel Expenses (人事費)
|
||||
[
|
||||
'account_code' => '5101',
|
||||
'account_name_zh' => '薪資支出',
|
||||
'account_name_en' => 'Salaries & Wages',
|
||||
'account_type' => 'expense',
|
||||
'category' => '人事費',
|
||||
'display_order' => 500,
|
||||
],
|
||||
[
|
||||
'account_code' => '5102',
|
||||
'account_name_zh' => '勞健保費',
|
||||
'account_name_en' => 'Labor & Health Insurance',
|
||||
'account_type' => 'expense',
|
||||
'category' => '人事費',
|
||||
'display_order' => 510,
|
||||
],
|
||||
[
|
||||
'account_code' => '5103',
|
||||
'account_name_zh' => '退休金提撥',
|
||||
'account_name_en' => 'Pension Contributions',
|
||||
'account_type' => 'expense',
|
||||
'category' => '人事費',
|
||||
'display_order' => 520,
|
||||
],
|
||||
[
|
||||
'account_code' => '5104',
|
||||
'account_name_zh' => '加班費',
|
||||
'account_name_en' => 'Overtime Pay',
|
||||
'account_type' => 'expense',
|
||||
'category' => '人事費',
|
||||
'display_order' => 530,
|
||||
],
|
||||
[
|
||||
'account_code' => '5105',
|
||||
'account_name_zh' => '員工福利',
|
||||
'account_name_en' => 'Employee Benefits',
|
||||
'account_type' => 'expense',
|
||||
'category' => '人事費',
|
||||
'display_order' => 540,
|
||||
],
|
||||
|
||||
// Operating Expenses (業務費)
|
||||
[
|
||||
'account_code' => '5201',
|
||||
'account_name_zh' => '租金支出',
|
||||
'account_name_en' => 'Rent Expense',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 550,
|
||||
],
|
||||
[
|
||||
'account_code' => '5202',
|
||||
'account_name_zh' => '水電費',
|
||||
'account_name_en' => 'Utilities',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 560,
|
||||
],
|
||||
[
|
||||
'account_code' => '5203',
|
||||
'account_name_zh' => '郵電費',
|
||||
'account_name_en' => 'Postage & Telecommunications',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 570,
|
||||
],
|
||||
[
|
||||
'account_code' => '5204',
|
||||
'account_name_zh' => '文具用品',
|
||||
'account_name_en' => 'Office Supplies',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 580,
|
||||
],
|
||||
[
|
||||
'account_code' => '5205',
|
||||
'account_name_zh' => '印刷費',
|
||||
'account_name_en' => 'Printing Expenses',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 590,
|
||||
],
|
||||
[
|
||||
'account_code' => '5206',
|
||||
'account_name_zh' => '旅運費',
|
||||
'account_name_en' => 'Travel & Transportation',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 600,
|
||||
],
|
||||
[
|
||||
'account_code' => '5207',
|
||||
'account_name_zh' => '保險費',
|
||||
'account_name_en' => 'Insurance Premiums',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 610,
|
||||
],
|
||||
[
|
||||
'account_code' => '5208',
|
||||
'account_name_zh' => '修繕費',
|
||||
'account_name_en' => 'Repairs & Maintenance',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 620,
|
||||
],
|
||||
[
|
||||
'account_code' => '5209',
|
||||
'account_name_zh' => '會議費',
|
||||
'account_name_en' => 'Meeting Expenses',
|
||||
'account_type' => 'expense',
|
||||
'category' => '業務費',
|
||||
'display_order' => 630,
|
||||
],
|
||||
|
||||
// Program/Activity Expenses (活動費)
|
||||
[
|
||||
'account_code' => '5301',
|
||||
'account_name_zh' => '活動場地費',
|
||||
'account_name_en' => 'Activity Venue Rental',
|
||||
'account_type' => 'expense',
|
||||
'category' => '活動費',
|
||||
'display_order' => 640,
|
||||
],
|
||||
[
|
||||
'account_code' => '5302',
|
||||
'account_name_zh' => '活動講師費',
|
||||
'account_name_en' => 'Speaker/Instructor Fees',
|
||||
'account_type' => 'expense',
|
||||
'category' => '活動費',
|
||||
'display_order' => 650,
|
||||
],
|
||||
[
|
||||
'account_code' => '5303',
|
||||
'account_name_zh' => '活動餐費',
|
||||
'account_name_en' => 'Activity Catering',
|
||||
'account_type' => 'expense',
|
||||
'category' => '活動費',
|
||||
'display_order' => 660,
|
||||
],
|
||||
[
|
||||
'account_code' => '5304',
|
||||
'account_name_zh' => '活動材料費',
|
||||
'account_name_en' => 'Activity Materials',
|
||||
'account_type' => 'expense',
|
||||
'category' => '活動費',
|
||||
'display_order' => 670,
|
||||
],
|
||||
[
|
||||
'account_code' => '5305',
|
||||
'account_name_zh' => '活動宣傳費',
|
||||
'account_name_en' => 'Activity Promotion',
|
||||
'account_type' => 'expense',
|
||||
'category' => '活動費',
|
||||
'display_order' => 680,
|
||||
],
|
||||
|
||||
// Administrative Expenses (行政管理費)
|
||||
[
|
||||
'account_code' => '5401',
|
||||
'account_name_zh' => '稅捐',
|
||||
'account_name_en' => 'Taxes',
|
||||
'account_type' => 'expense',
|
||||
'category' => '行政管理費',
|
||||
'display_order' => 690,
|
||||
],
|
||||
[
|
||||
'account_code' => '5402',
|
||||
'account_name_zh' => '規費',
|
||||
'account_name_en' => 'Administrative Fees',
|
||||
'account_type' => 'expense',
|
||||
'category' => '行政管理費',
|
||||
'display_order' => 700,
|
||||
],
|
||||
[
|
||||
'account_code' => '5403',
|
||||
'account_name_zh' => '銀行手續費',
|
||||
'account_name_en' => 'Bank Service Charges',
|
||||
'account_type' => 'expense',
|
||||
'category' => '行政管理費',
|
||||
'display_order' => 710,
|
||||
],
|
||||
[
|
||||
'account_code' => '5404',
|
||||
'account_name_zh' => '電腦網路費',
|
||||
'account_name_en' => 'IT & Network Expenses',
|
||||
'account_type' => 'expense',
|
||||
'category' => '行政管理費',
|
||||
'display_order' => 720,
|
||||
],
|
||||
[
|
||||
'account_code' => '5405',
|
||||
'account_name_zh' => '專業服務費',
|
||||
'account_name_en' => 'Professional Services',
|
||||
'account_type' => 'expense',
|
||||
'category' => '行政管理費',
|
||||
'display_order' => 730,
|
||||
'description' => '會計師、律師等專業服務費',
|
||||
],
|
||||
[
|
||||
'account_code' => '5406',
|
||||
'account_name_zh' => '折舊費用',
|
||||
'account_name_en' => 'Depreciation',
|
||||
'account_type' => 'expense',
|
||||
'category' => '行政管理費',
|
||||
'display_order' => 740,
|
||||
],
|
||||
|
||||
// Other Expenses (其他支出)
|
||||
[
|
||||
'account_code' => '5901',
|
||||
'account_name_zh' => '雜項支出',
|
||||
'account_name_en' => 'Miscellaneous Expenses',
|
||||
'account_type' => 'expense',
|
||||
'category' => '其他支出',
|
||||
'display_order' => 750,
|
||||
],
|
||||
[
|
||||
'account_code' => '5902',
|
||||
'account_name_zh' => '呆帳損失',
|
||||
'account_name_en' => 'Bad Debt Expense',
|
||||
'account_type' => 'expense',
|
||||
'category' => '其他支出',
|
||||
'display_order' => 760,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
ChartOfAccount::create($account);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
database/seeders/DatabaseSeeder.php
Normal file
22
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// \App\Models\User::factory(10)->create();
|
||||
|
||||
// \App\Models\User::factory()->create([
|
||||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// ]);
|
||||
}
|
||||
}
|
||||
75
database/seeders/DocumentCategorySeeder.php
Normal file
75
database/seeders/DocumentCategorySeeder.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\DocumentCategory;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DocumentCategorySeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$categories = [
|
||||
[
|
||||
'name' => '協會辦法',
|
||||
'slug' => 'bylaws',
|
||||
'description' => '協會章程、組織辦法等基本規範文件',
|
||||
'icon' => '📜',
|
||||
'sort_order' => 1,
|
||||
'default_access_level' => 'public',
|
||||
],
|
||||
[
|
||||
'name' => '法規與規範',
|
||||
'slug' => 'regulations',
|
||||
'description' => '內部規章、管理辦法、作業規範',
|
||||
'icon' => '📋',
|
||||
'sort_order' => 2,
|
||||
'default_access_level' => 'members',
|
||||
],
|
||||
[
|
||||
'name' => '會議記錄',
|
||||
'slug' => 'meeting-minutes',
|
||||
'description' => '理事會、會員大會、各委員會會議記錄',
|
||||
'icon' => '📝',
|
||||
'sort_order' => 3,
|
||||
'default_access_level' => 'members',
|
||||
],
|
||||
[
|
||||
'name' => '表格與申請書',
|
||||
'slug' => 'forms',
|
||||
'description' => '各類申請表格、會員服務表單',
|
||||
'icon' => '📄',
|
||||
'sort_order' => 4,
|
||||
'default_access_level' => 'public',
|
||||
],
|
||||
[
|
||||
'name' => '年度報告',
|
||||
'slug' => 'annual-reports',
|
||||
'description' => '年度工作報告、財務報告',
|
||||
'icon' => '📊',
|
||||
'sort_order' => 5,
|
||||
'default_access_level' => 'members',
|
||||
],
|
||||
[
|
||||
'name' => '活動文件',
|
||||
'slug' => 'events',
|
||||
'description' => '活動企劃、執行報告、相關文件',
|
||||
'icon' => '🎯',
|
||||
'sort_order' => 6,
|
||||
'default_access_level' => 'members',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
DocumentCategory::updateOrCreate(
|
||||
['slug' => $category['slug']],
|
||||
$category
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('Document categories seeded successfully!');
|
||||
}
|
||||
}
|
||||
176
database/seeders/FinancialWorkflowPermissionsSeeder.php
Normal file
176
database/seeders/FinancialWorkflowPermissionsSeeder.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class FinancialWorkflowPermissionsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Create permissions for financial workflow
|
||||
$permissions = [
|
||||
// Approval Stage Permissions
|
||||
'approve_finance_cashier' => '出納審核財務申請單(第一階段)',
|
||||
'approve_finance_accountant' => '會計審核財務申請單(第二階段)',
|
||||
'approve_finance_chair' => '理事長審核財務申請單(第三階段)',
|
||||
'approve_finance_board' => '理事會審核大額財務申請(大於50,000)',
|
||||
|
||||
// Payment Stage Permissions
|
||||
'create_payment_order' => '會計製作付款單',
|
||||
'verify_payment_order' => '出納覆核付款單',
|
||||
'execute_payment' => '出納執行付款',
|
||||
'upload_payment_receipt' => '上傳付款憑證',
|
||||
|
||||
// Recording Stage Permissions
|
||||
'record_cashier_ledger' => '出納記錄現金簿',
|
||||
'record_accounting_transaction' => '會計記錄會計分錄',
|
||||
'view_cashier_ledger' => '查看出納現金簿',
|
||||
'view_accounting_transactions' => '查看會計分錄',
|
||||
|
||||
// Reconciliation Permissions
|
||||
'prepare_bank_reconciliation' => '出納製作銀行調節表',
|
||||
'review_bank_reconciliation' => '會計覆核銀行調節表',
|
||||
'approve_bank_reconciliation' => '主管核准銀行調節表',
|
||||
|
||||
// General Finance Document Permissions
|
||||
'view_finance_documents' => '查看財務申請單',
|
||||
'create_finance_documents' => '建立財務申請單',
|
||||
'edit_finance_documents' => '編輯財務申請單',
|
||||
'delete_finance_documents' => '刪除財務申請單',
|
||||
|
||||
// Chart of Accounts & Budget Permissions
|
||||
'assign_chart_of_account' => '指定會計科目',
|
||||
'assign_budget_item' => '指定預算項目',
|
||||
|
||||
// Dashboard & Reports Permissions
|
||||
'view_finance_dashboard' => '查看財務儀表板',
|
||||
'view_finance_reports' => '查看財務報表',
|
||||
'export_finance_reports' => '匯出財務報表',
|
||||
];
|
||||
|
||||
foreach ($permissions as $name => $description) {
|
||||
Permission::firstOrCreate(
|
||||
['name' => $name],
|
||||
['guard_name' => 'web']
|
||||
);
|
||||
$this->command->info("Permission created: {$name}");
|
||||
}
|
||||
|
||||
// Create roles for financial workflow
|
||||
$roles = [
|
||||
'finance_cashier' => [
|
||||
'permissions' => [
|
||||
// Approval stage
|
||||
'approve_finance_cashier',
|
||||
// Payment stage
|
||||
'verify_payment_order',
|
||||
'execute_payment',
|
||||
'upload_payment_receipt',
|
||||
// Recording stage
|
||||
'record_cashier_ledger',
|
||||
'view_cashier_ledger',
|
||||
// Reconciliation
|
||||
'prepare_bank_reconciliation',
|
||||
// General
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
],
|
||||
'description' => '出納 - 管錢(覆核付款單、執行付款、記錄現金簿、製作銀行調節表)',
|
||||
],
|
||||
'finance_accountant' => [
|
||||
'permissions' => [
|
||||
// Approval stage
|
||||
'approve_finance_accountant',
|
||||
// Payment stage
|
||||
'create_payment_order',
|
||||
// Recording stage
|
||||
'record_accounting_transaction',
|
||||
'view_accounting_transactions',
|
||||
// Reconciliation
|
||||
'review_bank_reconciliation',
|
||||
// Chart of accounts & budget
|
||||
'assign_chart_of_account',
|
||||
'assign_budget_item',
|
||||
// General
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
'view_finance_reports',
|
||||
'export_finance_reports',
|
||||
],
|
||||
'description' => '會計 - 管帳(製作付款單、記錄會計分錄、覆核銀行調節表、指定會計科目)',
|
||||
],
|
||||
'finance_chair' => [
|
||||
'permissions' => [
|
||||
// Approval stage
|
||||
'approve_finance_chair',
|
||||
// Reconciliation
|
||||
'approve_bank_reconciliation',
|
||||
// General
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
'view_finance_reports',
|
||||
'export_finance_reports',
|
||||
],
|
||||
'description' => '理事長 - 審核中大額財務申請、核准銀行調節表',
|
||||
],
|
||||
'finance_board_member' => [
|
||||
'permissions' => [
|
||||
// Approval stage (for large amounts)
|
||||
'approve_finance_board',
|
||||
// General
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
'view_finance_reports',
|
||||
],
|
||||
'description' => '理事 - 審核大額財務申請(大於50,000)',
|
||||
],
|
||||
'finance_requester' => [
|
||||
'permissions' => [
|
||||
'view_finance_documents',
|
||||
'create_finance_documents',
|
||||
'edit_finance_documents',
|
||||
],
|
||||
'description' => '財務申請人 - 可建立和編輯自己的財務申請單',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($roles as $roleName => $roleData) {
|
||||
$role = Role::firstOrCreate(
|
||||
['name' => $roleName],
|
||||
['guard_name' => 'web']
|
||||
);
|
||||
|
||||
// Assign permissions to role
|
||||
$role->syncPermissions($roleData['permissions']);
|
||||
|
||||
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
|
||||
}
|
||||
|
||||
// Assign all financial workflow permissions to admin role (if exists)
|
||||
$adminRole = Role::where('name', 'admin')->first();
|
||||
if ($adminRole) {
|
||||
$adminRole->givePermissionTo(array_keys($permissions));
|
||||
$this->command->info("Admin role updated with all financial workflow permissions");
|
||||
}
|
||||
|
||||
$this->command->info("\n=== Financial Workflow Roles & Permissions Created ===");
|
||||
$this->command->info("Roles created:");
|
||||
$this->command->info("1. finance_cashier - 出納(管錢)");
|
||||
$this->command->info("2. finance_accountant - 會計(管帳)");
|
||||
$this->command->info("3. finance_chair - 理事長");
|
||||
$this->command->info("4. finance_board_member - 理事");
|
||||
$this->command->info("5. finance_requester - 財務申請人");
|
||||
$this->command->info("\nWorkflow stages:");
|
||||
$this->command->info("1. Approval Stage: Cashier → Accountant → Chair (→ Board for large amounts)");
|
||||
$this->command->info("2. Payment Stage: Accountant creates order → Cashier verifies → Cashier executes");
|
||||
$this->command->info("3. Recording Stage: Cashier records ledger + Accountant records transactions");
|
||||
$this->command->info("4. Reconciliation: Cashier prepares → Accountant reviews → Chair approves");
|
||||
}
|
||||
}
|
||||
393
database/seeders/FinancialWorkflowTestDataSeeder.php
Normal file
393
database/seeders/FinancialWorkflowTestDataSeeder.php
Normal file
@@ -0,0 +1,393 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\BankReconciliation;
|
||||
use App\Models\CashierLedgerEntry;
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\PaymentOrder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
/**
|
||||
* Financial Workflow Test Data Seeder
|
||||
*
|
||||
* Generates comprehensive test data for the financial workflow system
|
||||
*/
|
||||
class FinancialWorkflowTestDataSeeder extends Seeder
|
||||
{
|
||||
protected User $requester;
|
||||
protected User $cashier;
|
||||
protected User $accountant;
|
||||
protected User $chair;
|
||||
protected User $boardMember;
|
||||
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('🌱 Seeding financial workflow test data...');
|
||||
|
||||
// Create or get test users
|
||||
$this->createTestUsers();
|
||||
|
||||
// Seed finance documents at various stages
|
||||
$this->seedFinanceDocuments();
|
||||
|
||||
// Seed payment orders
|
||||
$this->seedPaymentOrders();
|
||||
|
||||
// Seed cashier ledger entries
|
||||
$this->seedCashierLedgerEntries();
|
||||
|
||||
// Seed bank reconciliations
|
||||
$this->seedBankReconciliations();
|
||||
|
||||
$this->command->info('✅ Financial workflow test data seeded successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test users with appropriate roles
|
||||
*/
|
||||
protected function createTestUsers(): void
|
||||
{
|
||||
$this->command->info('Creating test users...');
|
||||
|
||||
$this->requester = User::firstOrCreate(
|
||||
['email' => 'requester@test.com'],
|
||||
[
|
||||
'name' => 'Test Requester',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$this->requester->assignRole('finance_requester');
|
||||
|
||||
$this->cashier = User::firstOrCreate(
|
||||
['email' => 'cashier@test.com'],
|
||||
[
|
||||
'name' => 'Test Cashier',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$this->cashier->assignRole('finance_cashier');
|
||||
|
||||
$this->accountant = User::firstOrCreate(
|
||||
['email' => 'accountant@test.com'],
|
||||
[
|
||||
'name' => 'Test Accountant',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$this->accountant->assignRole('finance_accountant');
|
||||
|
||||
$this->chair = User::firstOrCreate(
|
||||
['email' => 'chair@test.com'],
|
||||
[
|
||||
'name' => 'Test Chair',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$this->chair->assignRole('finance_chair');
|
||||
|
||||
$this->boardMember = User::firstOrCreate(
|
||||
['email' => 'board@test.com'],
|
||||
[
|
||||
'name' => 'Test Board Member',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$this->boardMember->assignRole('finance_board_member');
|
||||
|
||||
$this->command->info('✓ Test users created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed finance documents at various stages of the workflow
|
||||
*/
|
||||
protected function seedFinanceDocuments(): void
|
||||
{
|
||||
$this->command->info('Seeding finance documents...');
|
||||
|
||||
// Pending documents (Stage 1)
|
||||
FinanceDocument::factory()
|
||||
->count(3)
|
||||
->smallAmount()
|
||||
->pending()
|
||||
->create(['submitted_by_id' => $this->requester->id]);
|
||||
|
||||
FinanceDocument::factory()
|
||||
->count(2)
|
||||
->mediumAmount()
|
||||
->pending()
|
||||
->create(['submitted_by_id' => $this->requester->id]);
|
||||
|
||||
// Approved by cashier (Stage 1)
|
||||
FinanceDocument::factory()
|
||||
->count(2)
|
||||
->smallAmount()
|
||||
->approvedByCashier()
|
||||
->create([
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'cashier_approved_by_id' => $this->cashier->id,
|
||||
]);
|
||||
|
||||
// Approved by accountant - small amounts (Ready for payment)
|
||||
FinanceDocument::factory()
|
||||
->count(3)
|
||||
->smallAmount()
|
||||
->approvedByAccountant()
|
||||
->create([
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'cashier_approved_by_id' => $this->cashier->id,
|
||||
'accountant_approved_by_id' => $this->accountant->id,
|
||||
]);
|
||||
|
||||
// Approved by chair - medium amounts (Ready for payment)
|
||||
FinanceDocument::factory()
|
||||
->count(2)
|
||||
->mediumAmount()
|
||||
->approvedByChair()
|
||||
->create([
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'cashier_approved_by_id' => $this->cashier->id,
|
||||
'accountant_approved_by_id' => $this->accountant->id,
|
||||
'chair_approved_by_id' => $this->chair->id,
|
||||
]);
|
||||
|
||||
// Large amount with board approval (Ready for payment)
|
||||
FinanceDocument::factory()
|
||||
->count(1)
|
||||
->largeAmount()
|
||||
->approvedByChair()
|
||||
->create([
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'cashier_approved_by_id' => $this->cashier->id,
|
||||
'accountant_approved_by_id' => $this->accountant->id,
|
||||
'chair_approved_by_id' => $this->chair->id,
|
||||
'board_meeting_approved_at' => now(),
|
||||
'board_meeting_approved_by_id' => $this->boardMember->id,
|
||||
]);
|
||||
|
||||
// Completed workflow
|
||||
FinanceDocument::factory()
|
||||
->count(5)
|
||||
->smallAmount()
|
||||
->approvedByAccountant()
|
||||
->paymentExecuted()
|
||||
->create([
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'cashier_approved_by_id' => $this->cashier->id,
|
||||
'accountant_approved_by_id' => $this->accountant->id,
|
||||
'cashier_recorded_at' => now(),
|
||||
]);
|
||||
|
||||
// Rejected documents
|
||||
FinanceDocument::factory()
|
||||
->count(2)
|
||||
->rejected()
|
||||
->create([
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
]);
|
||||
|
||||
$this->command->info('✓ Finance documents seeded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed payment orders
|
||||
*/
|
||||
protected function seedPaymentOrders(): void
|
||||
{
|
||||
$this->command->info('Seeding payment orders...');
|
||||
|
||||
// Get approved documents without payment orders
|
||||
$approvedDocs = FinanceDocument::whereIn('status', [
|
||||
FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
|
||||
FinanceDocument::STATUS_APPROVED_CHAIR,
|
||||
])
|
||||
->whereNull('payment_order_created_at')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
foreach ($approvedDocs as $doc) {
|
||||
// Pending verification
|
||||
PaymentOrder::factory()
|
||||
->pendingVerification()
|
||||
->create([
|
||||
'finance_document_id' => $doc->id,
|
||||
'payment_amount' => $doc->amount,
|
||||
'created_by_accountant_id' => $this->accountant->id,
|
||||
]);
|
||||
|
||||
$doc->update([
|
||||
'payment_order_created_at' => now(),
|
||||
'payment_order_created_by_id' => $this->accountant->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Verified payment orders
|
||||
PaymentOrder::factory()
|
||||
->count(3)
|
||||
->verified()
|
||||
->create([
|
||||
'created_by_accountant_id' => $this->accountant->id,
|
||||
'verified_by_cashier_id' => $this->cashier->id,
|
||||
]);
|
||||
|
||||
// Executed payment orders
|
||||
PaymentOrder::factory()
|
||||
->count(5)
|
||||
->executed()
|
||||
->create([
|
||||
'created_by_accountant_id' => $this->accountant->id,
|
||||
'verified_by_cashier_id' => $this->cashier->id,
|
||||
'executed_by_cashier_id' => $this->cashier->id,
|
||||
]);
|
||||
|
||||
$this->command->info('✓ Payment orders seeded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed cashier ledger entries with running balances
|
||||
*/
|
||||
protected function seedCashierLedgerEntries(): void
|
||||
{
|
||||
$this->command->info('Seeding cashier ledger entries...');
|
||||
|
||||
$bankAccounts = [
|
||||
'First Bank - 1234567890',
|
||||
'Second Bank - 0987654321',
|
||||
'Petty Cash',
|
||||
];
|
||||
|
||||
foreach ($bankAccounts as $account) {
|
||||
$currentBalance = 100000; // Starting balance
|
||||
|
||||
// Create 10 entries for each account
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$isReceipt = $i % 3 !== 0; // 2/3 receipts, 1/3 payments
|
||||
$amount = rand(1000, 10000);
|
||||
|
||||
$entry = CashierLedgerEntry::create([
|
||||
'entry_type' => $isReceipt ? 'receipt' : 'payment',
|
||||
'entry_date' => now()->subDays(rand(1, 30)),
|
||||
'amount' => $amount,
|
||||
'payment_method' => $account === 'Petty Cash' ? 'cash' : 'bank_transfer',
|
||||
'bank_account' => $account,
|
||||
'balance_before' => $currentBalance,
|
||||
'balance_after' => $isReceipt
|
||||
? $currentBalance + $amount
|
||||
: $currentBalance - $amount,
|
||||
'receipt_number' => $isReceipt ? 'RCP' . str_pad($i + 1, 6, '0', STR_PAD_LEFT) : null,
|
||||
'notes' => $isReceipt ? 'Test receipt entry' : 'Test payment entry',
|
||||
'recorded_by_cashier_id' => $this->cashier->id,
|
||||
'recorded_at' => now()->subDays(rand(1, 30)),
|
||||
]);
|
||||
|
||||
$currentBalance = $entry->balance_after;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info('✓ Cashier ledger entries seeded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed bank reconciliations
|
||||
*/
|
||||
protected function seedBankReconciliations(): void
|
||||
{
|
||||
$this->command->info('Seeding bank reconciliations...');
|
||||
|
||||
// Pending reconciliation
|
||||
BankReconciliation::create([
|
||||
'reconciliation_month' => now()->startOfMonth(),
|
||||
'bank_statement_date' => now(),
|
||||
'bank_statement_balance' => 100000,
|
||||
'system_book_balance' => 95000,
|
||||
'outstanding_checks' => [
|
||||
['check_number' => 'CHK001', 'amount' => 3000, 'description' => 'Vendor A payment'],
|
||||
['check_number' => 'CHK002', 'amount' => 2000, 'description' => 'Service fee'],
|
||||
],
|
||||
'deposits_in_transit' => [
|
||||
['date' => now()->format('Y-m-d'), 'amount' => 5000, 'description' => 'Member dues'],
|
||||
],
|
||||
'bank_charges' => [
|
||||
['amount' => 500, 'description' => 'Monthly service charge'],
|
||||
],
|
||||
'discrepancy_amount' => 4500,
|
||||
'notes' => 'Pending review',
|
||||
'prepared_by_cashier_id' => $this->cashier->id,
|
||||
'prepared_at' => now(),
|
||||
'reconciliation_status' => 'pending',
|
||||
]);
|
||||
|
||||
// Reviewed reconciliation
|
||||
BankReconciliation::create([
|
||||
'reconciliation_month' => now()->subMonth()->startOfMonth(),
|
||||
'bank_statement_date' => now()->subMonth(),
|
||||
'bank_statement_balance' => 95000,
|
||||
'system_book_balance' => 93000,
|
||||
'outstanding_checks' => [
|
||||
['check_number' => 'CHK003', 'amount' => 1500, 'description' => 'Supplies'],
|
||||
],
|
||||
'deposits_in_transit' => [
|
||||
['date' => now()->subMonth()->format('Y-m-d'), 'amount' => 3000, 'description' => 'Donation'],
|
||||
],
|
||||
'bank_charges' => [
|
||||
['amount' => 500, 'description' => 'Service charge'],
|
||||
],
|
||||
'discrepancy_amount' => 0,
|
||||
'notes' => 'All items reconciled',
|
||||
'prepared_by_cashier_id' => $this->cashier->id,
|
||||
'prepared_at' => now()->subMonth(),
|
||||
'reviewed_by_accountant_id' => $this->accountant->id,
|
||||
'reviewed_at' => now()->subMonth()->addDays(2),
|
||||
'reconciliation_status' => 'pending',
|
||||
]);
|
||||
|
||||
// Completed reconciliation
|
||||
BankReconciliation::create([
|
||||
'reconciliation_month' => now()->subMonths(2)->startOfMonth(),
|
||||
'bank_statement_date' => now()->subMonths(2),
|
||||
'bank_statement_balance' => 90000,
|
||||
'system_book_balance' => 90000,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [
|
||||
['amount' => 500, 'description' => 'Service charge'],
|
||||
],
|
||||
'discrepancy_amount' => 0,
|
||||
'notes' => 'Perfect match',
|
||||
'prepared_by_cashier_id' => $this->cashier->id,
|
||||
'prepared_at' => now()->subMonths(2),
|
||||
'reviewed_by_accountant_id' => $this->accountant->id,
|
||||
'reviewed_at' => now()->subMonths(2)->addDays(2),
|
||||
'approved_by_manager_id' => $this->chair->id,
|
||||
'approved_at' => now()->subMonths(2)->addDays(3),
|
||||
'reconciliation_status' => 'completed',
|
||||
]);
|
||||
|
||||
// Reconciliation with discrepancy
|
||||
BankReconciliation::create([
|
||||
'reconciliation_month' => now()->subMonths(3)->startOfMonth(),
|
||||
'bank_statement_date' => now()->subMonths(3),
|
||||
'bank_statement_balance' => 85000,
|
||||
'system_book_balance' => 75000,
|
||||
'outstanding_checks' => [
|
||||
['check_number' => 'CHK004', 'amount' => 2000, 'description' => 'Payment'],
|
||||
],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [
|
||||
['amount' => 500, 'description' => 'Service charge'],
|
||||
],
|
||||
'discrepancy_amount' => 7500,
|
||||
'notes' => 'Large discrepancy - needs investigation',
|
||||
'prepared_by_cashier_id' => $this->cashier->id,
|
||||
'prepared_at' => now()->subMonths(3),
|
||||
'reconciliation_status' => 'discrepancy',
|
||||
]);
|
||||
|
||||
$this->command->info('✓ Bank reconciliations seeded');
|
||||
}
|
||||
}
|
||||
86
database/seeders/IssueLabelSeeder.php
Normal file
86
database/seeders/IssueLabelSeeder.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\IssueLabel;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class IssueLabelSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$labels = [
|
||||
[
|
||||
'name' => 'urgent',
|
||||
'color' => '#DC2626',
|
||||
'description' => 'Requires immediate attention',
|
||||
],
|
||||
[
|
||||
'name' => 'bug',
|
||||
'color' => '#EF4444',
|
||||
'description' => 'Something is not working correctly',
|
||||
],
|
||||
[
|
||||
'name' => 'enhancement',
|
||||
'color' => '#3B82F6',
|
||||
'description' => 'New feature or improvement request',
|
||||
],
|
||||
[
|
||||
'name' => 'documentation',
|
||||
'color' => '#10B981',
|
||||
'description' => 'Documentation related task',
|
||||
],
|
||||
[
|
||||
'name' => 'member-facing',
|
||||
'color' => '#8B5CF6',
|
||||
'description' => 'Affects members directly',
|
||||
],
|
||||
[
|
||||
'name' => 'internal',
|
||||
'color' => '#F59E0B',
|
||||
'description' => 'Internal staff operations',
|
||||
],
|
||||
[
|
||||
'name' => 'event',
|
||||
'color' => '#EC4899',
|
||||
'description' => 'Event planning or execution',
|
||||
],
|
||||
[
|
||||
'name' => 'finance',
|
||||
'color' => '#14B8A6',
|
||||
'description' => 'Financial or budget related',
|
||||
],
|
||||
[
|
||||
'name' => 'communications',
|
||||
'color' => '#6366F1',
|
||||
'description' => 'Marketing, PR, or communications',
|
||||
],
|
||||
[
|
||||
'name' => 'blocked',
|
||||
'color' => '#64748B',
|
||||
'description' => 'Blocked by another issue or dependency',
|
||||
],
|
||||
[
|
||||
'name' => 'help-wanted',
|
||||
'color' => '#22C55E',
|
||||
'description' => 'Looking for volunteers or assistance',
|
||||
],
|
||||
[
|
||||
'name' => 'question',
|
||||
'color' => '#A855F7',
|
||||
'description' => 'Question or clarification needed',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($labels as $label) {
|
||||
IssueLabel::updateOrCreate(
|
||||
['name' => $label['name']],
|
||||
$label
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
database/seeders/PaymentVerificationRolesSeeder.php
Normal file
79
database/seeders/PaymentVerificationRolesSeeder.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class PaymentVerificationRolesSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Create permissions for payment verification workflow
|
||||
$permissions = [
|
||||
'verify_payments_cashier' => 'Verify membership payments as cashier (Tier 1)',
|
||||
'verify_payments_accountant' => 'Verify membership payments as accountant (Tier 2)',
|
||||
'verify_payments_chair' => 'Verify membership payments as chair (Tier 3)',
|
||||
'activate_memberships' => 'Activate member accounts after payment approval',
|
||||
'view_payment_verifications' => 'View payment verification dashboard',
|
||||
];
|
||||
|
||||
foreach ($permissions as $name => $description) {
|
||||
Permission::firstOrCreate(
|
||||
['name' => $name],
|
||||
['guard_name' => 'web']
|
||||
);
|
||||
$this->command->info("Permission created: {$name}");
|
||||
}
|
||||
|
||||
// Create roles for payment verification
|
||||
$roles = [
|
||||
'payment_cashier' => [
|
||||
'permissions' => ['verify_payments_cashier', 'view_payment_verifications'],
|
||||
'description' => 'Cashier - First tier payment verification',
|
||||
],
|
||||
'payment_accountant' => [
|
||||
'permissions' => ['verify_payments_accountant', 'view_payment_verifications'],
|
||||
'description' => 'Accountant - Second tier payment verification',
|
||||
],
|
||||
'payment_chair' => [
|
||||
'permissions' => ['verify_payments_chair', 'view_payment_verifications'],
|
||||
'description' => 'Chair - Final tier payment verification',
|
||||
],
|
||||
'membership_manager' => [
|
||||
'permissions' => ['activate_memberships', 'view_payment_verifications'],
|
||||
'description' => 'Membership Manager - Can activate memberships after approval',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($roles as $roleName => $roleData) {
|
||||
$role = Role::firstOrCreate(
|
||||
['name' => $roleName],
|
||||
['guard_name' => 'web']
|
||||
);
|
||||
|
||||
// Assign permissions to role
|
||||
$role->syncPermissions($roleData['permissions']);
|
||||
|
||||
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
|
||||
}
|
||||
|
||||
// Assign all payment verification permissions to admin role (if exists)
|
||||
$adminRole = Role::where('name', 'admin')->first();
|
||||
if ($adminRole) {
|
||||
$adminRole->givePermissionTo([
|
||||
'verify_payments_cashier',
|
||||
'verify_payments_accountant',
|
||||
'verify_payments_chair',
|
||||
'activate_memberships',
|
||||
'view_payment_verifications',
|
||||
]);
|
||||
$this->command->info("Admin role updated with all payment verification permissions");
|
||||
}
|
||||
}
|
||||
}
|
||||
27
database/seeders/RoleSeeder.php
Normal file
27
database/seeders/RoleSeeder.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RoleSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$roles = [
|
||||
'admin' => 'Full system administrator',
|
||||
'staff' => 'General staff with access to internal tools',
|
||||
'cashier' => 'Handles payment recording and finance intake',
|
||||
'accountant' => 'Reviews finance docs and approvals',
|
||||
'chair' => 'Board chairperson for final approvals',
|
||||
];
|
||||
|
||||
collect($roles)->each(function ($description, $role) {
|
||||
Role::updateOrCreate(
|
||||
['name' => $role, 'guard_name' => 'web'],
|
||||
['description' => $description]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
240
database/seeders/SystemSettingsSeeder.php
Normal file
240
database/seeders/SystemSettingsSeeder.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SystemSettingsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$settings = [
|
||||
// General Settings
|
||||
[
|
||||
'key' => 'general.system_name',
|
||||
'value' => 'Usher Management System',
|
||||
'type' => 'string',
|
||||
'group' => 'general',
|
||||
'description' => 'System name displayed throughout the application'
|
||||
],
|
||||
[
|
||||
'key' => 'general.timezone',
|
||||
'value' => 'Asia/Taipei',
|
||||
'type' => 'string',
|
||||
'group' => 'general',
|
||||
'description' => 'System timezone'
|
||||
],
|
||||
|
||||
// Document Features
|
||||
[
|
||||
'key' => 'features.qr_codes_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'features',
|
||||
'description' => 'Enable QR code generation for documents'
|
||||
],
|
||||
[
|
||||
'key' => 'features.tagging_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'features',
|
||||
'description' => 'Enable document tagging system'
|
||||
],
|
||||
[
|
||||
'key' => 'features.expiration_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'features',
|
||||
'description' => 'Enable document expiration dates and auto-archive'
|
||||
],
|
||||
[
|
||||
'key' => 'features.bulk_import_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'features',
|
||||
'description' => 'Enable bulk document import feature'
|
||||
],
|
||||
[
|
||||
'key' => 'features.statistics_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'features',
|
||||
'description' => 'Enable document statistics dashboard'
|
||||
],
|
||||
[
|
||||
'key' => 'features.version_history_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'features',
|
||||
'description' => 'Enable document version history tracking'
|
||||
],
|
||||
|
||||
// Security & Limits
|
||||
[
|
||||
'key' => 'security.rate_limit_authenticated',
|
||||
'value' => '50',
|
||||
'type' => 'integer',
|
||||
'group' => 'security',
|
||||
'description' => 'Downloads per hour for authenticated users'
|
||||
],
|
||||
[
|
||||
'key' => 'security.rate_limit_guest',
|
||||
'value' => '10',
|
||||
'type' => 'integer',
|
||||
'group' => 'security',
|
||||
'description' => 'Downloads per hour for guest users'
|
||||
],
|
||||
[
|
||||
'key' => 'security.max_file_size_mb',
|
||||
'value' => '10',
|
||||
'type' => 'integer',
|
||||
'group' => 'security',
|
||||
'description' => 'Maximum file upload size in MB'
|
||||
],
|
||||
[
|
||||
'key' => 'security.allowed_file_types',
|
||||
'value' => json_encode(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'jpeg', 'png']),
|
||||
'type' => 'json',
|
||||
'group' => 'security',
|
||||
'description' => 'Allowed file types for uploads'
|
||||
],
|
||||
|
||||
// Document Settings
|
||||
[
|
||||
'key' => 'documents.default_access_level',
|
||||
'value' => 'members',
|
||||
'type' => 'string',
|
||||
'group' => 'documents',
|
||||
'description' => 'Default access level for new documents (public, members, admin, board)'
|
||||
],
|
||||
[
|
||||
'key' => 'documents.default_expiration_days',
|
||||
'value' => '90',
|
||||
'type' => 'integer',
|
||||
'group' => 'documents',
|
||||
'description' => 'Default expiration period in days (0 = no expiration)'
|
||||
],
|
||||
[
|
||||
'key' => 'documents.expiration_warning_days',
|
||||
'value' => '30',
|
||||
'type' => 'integer',
|
||||
'group' => 'documents',
|
||||
'description' => 'Days before expiration to show warning'
|
||||
],
|
||||
[
|
||||
'key' => 'documents.auto_archive_enabled',
|
||||
'value' => '0',
|
||||
'type' => 'boolean',
|
||||
'group' => 'documents',
|
||||
'description' => 'Automatically archive expired documents'
|
||||
],
|
||||
[
|
||||
'key' => 'documents.max_tags_per_document',
|
||||
'value' => '10',
|
||||
'type' => 'integer',
|
||||
'group' => 'documents',
|
||||
'description' => 'Maximum number of tags per document'
|
||||
],
|
||||
|
||||
// Notifications
|
||||
[
|
||||
'key' => 'notifications.enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'notifications',
|
||||
'description' => 'Enable email notifications'
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.expiration_alerts_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'notifications',
|
||||
'description' => 'Send email alerts for expiring documents'
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.expiration_recipients',
|
||||
'value' => json_encode([]),
|
||||
'type' => 'json',
|
||||
'group' => 'notifications',
|
||||
'description' => 'Email recipients for expiration alerts'
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.archive_notifications_enabled',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'group' => 'notifications',
|
||||
'description' => 'Send notifications when documents are auto-archived'
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.new_document_alerts_enabled',
|
||||
'value' => '0',
|
||||
'type' => 'boolean',
|
||||
'group' => 'notifications',
|
||||
'description' => 'Send alerts when new documents are uploaded'
|
||||
],
|
||||
|
||||
// Advanced Settings
|
||||
[
|
||||
'key' => 'advanced.qr_code_size',
|
||||
'value' => '300',
|
||||
'type' => 'integer',
|
||||
'group' => 'advanced',
|
||||
'description' => 'QR code size in pixels'
|
||||
],
|
||||
[
|
||||
'key' => 'advanced.qr_code_format',
|
||||
'value' => 'png',
|
||||
'type' => 'string',
|
||||
'group' => 'advanced',
|
||||
'description' => 'QR code format (png or svg)'
|
||||
],
|
||||
[
|
||||
'key' => 'advanced.statistics_time_range',
|
||||
'value' => '30',
|
||||
'type' => 'integer',
|
||||
'group' => 'advanced',
|
||||
'description' => 'Default time range for statistics in days'
|
||||
],
|
||||
[
|
||||
'key' => 'advanced.statistics_top_n',
|
||||
'value' => '10',
|
||||
'type' => 'integer',
|
||||
'group' => 'advanced',
|
||||
'description' => 'Number of top items to display in statistics'
|
||||
],
|
||||
[
|
||||
'key' => 'advanced.audit_log_retention_days',
|
||||
'value' => '365',
|
||||
'type' => 'integer',
|
||||
'group' => 'advanced',
|
||||
'description' => 'How long to retain audit logs in days'
|
||||
],
|
||||
[
|
||||
'key' => 'advanced.max_versions_retain',
|
||||
'value' => '0',
|
||||
'type' => 'integer',
|
||||
'group' => 'advanced',
|
||||
'description' => 'Maximum versions to retain per document (0 = unlimited)'
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($settings as $settingData) {
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => $settingData['key']],
|
||||
[
|
||||
'value' => $settingData['value'],
|
||||
'type' => $settingData['type'],
|
||||
'group' => $settingData['group'],
|
||||
'description' => $settingData['description'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('System settings seeded successfully (' . count($settings) . ' settings)');
|
||||
}
|
||||
}
|
||||
769
database/seeders/TestDataSeeder.php
Normal file
769
database/seeders/TestDataSeeder.php
Normal file
@@ -0,0 +1,769 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Budget;
|
||||
use App\Models\BudgetItem;
|
||||
use App\Models\ChartOfAccount;
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\Issue;
|
||||
use App\Models\IssueComment;
|
||||
use App\Models\IssueLabel;
|
||||
use App\Models\IssueTimeLog;
|
||||
use App\Models\Member;
|
||||
use App\Models\MembershipPayment;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class TestDataSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* Creates comprehensive test data for manual and automated testing:
|
||||
* - 5 test users with different roles
|
||||
* - 20 members in various states (pending, active, expired, suspended)
|
||||
* - 30 payments at different approval stages
|
||||
* - 15 issues with various statuses
|
||||
* - 5 budgets with items
|
||||
* - 10 finance documents
|
||||
* - Sample transactions
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command->info('🌱 Starting Test Data Seeding...');
|
||||
|
||||
// Ensure required seeders have run
|
||||
$this->call([
|
||||
RoleSeeder::class,
|
||||
PaymentVerificationRolesSeeder::class,
|
||||
ChartOfAccountSeeder::class,
|
||||
IssueLabelSeeder::class,
|
||||
]);
|
||||
|
||||
// Create test users with different roles
|
||||
$users = $this->createTestUsers();
|
||||
$this->command->info('✅ Created 6 test users with different roles');
|
||||
|
||||
// Create members in various states
|
||||
$members = $this->createTestMembers($users);
|
||||
$this->command->info('✅ Created 20 members in various membership states');
|
||||
|
||||
// Create payments at different approval stages
|
||||
$payments = $this->createTestPayments($members, $users);
|
||||
$this->command->info('✅ Created 30 membership payments at different approval stages');
|
||||
|
||||
// Create issues with various statuses
|
||||
$issues = $this->createTestIssues($users, $members);
|
||||
$this->command->info('✅ Created 15 issues with various statuses and relationships');
|
||||
|
||||
// Create budgets with items
|
||||
$budgets = $this->createTestBudgets($users);
|
||||
$this->command->info('✅ Created 5 budgets with budget items in different states');
|
||||
|
||||
// Create finance documents
|
||||
$financeDocuments = $this->createTestFinanceDocuments($users);
|
||||
$this->command->info('✅ Created 10 finance documents');
|
||||
|
||||
// Create sample transactions
|
||||
$transactions = $this->createTestTransactions($users);
|
||||
$this->command->info('✅ Created sample transactions');
|
||||
|
||||
$this->command->info('');
|
||||
$this->command->info('🎉 Test Data Seeding Complete!');
|
||||
$this->command->info('');
|
||||
$this->displayTestAccounts($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test users with different roles
|
||||
*/
|
||||
private function createTestUsers(): array
|
||||
{
|
||||
$users = [];
|
||||
|
||||
// 1. Super Admin
|
||||
$admin = User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$admin->assignRole('admin');
|
||||
$users['admin'] = $admin;
|
||||
|
||||
// 2. Payment Cashier
|
||||
$cashier = User::create([
|
||||
'name' => 'Cashier User',
|
||||
'email' => 'cashier@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$cashier->assignRole('payment_cashier');
|
||||
$users['cashier'] = $cashier;
|
||||
|
||||
// 3. Payment Accountant
|
||||
$accountant = User::create([
|
||||
'name' => 'Accountant User',
|
||||
'email' => 'accountant@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$accountant->assignRole('payment_accountant');
|
||||
$users['accountant'] = $accountant;
|
||||
|
||||
// 4. Payment Chair
|
||||
$chair = User::create([
|
||||
'name' => 'Chair User',
|
||||
'email' => 'chair@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$chair->assignRole('payment_chair');
|
||||
$users['chair'] = $chair;
|
||||
|
||||
// 5. Membership Manager
|
||||
$manager = User::create([
|
||||
'name' => 'Membership Manager',
|
||||
'email' => 'manager@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$manager->assignRole('membership_manager');
|
||||
$users['manager'] = $manager;
|
||||
|
||||
// 6. Regular Member User
|
||||
$member = User::create([
|
||||
'name' => 'Regular Member',
|
||||
'email' => 'member@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => false,
|
||||
]);
|
||||
$users['member'] = $member;
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test members in various states
|
||||
*/
|
||||
private function createTestMembers(array $users): array
|
||||
{
|
||||
$members = [];
|
||||
$taiwanCities = ['台北市', '新北市', '台中市', '台南市', '高雄市', '桃園市'];
|
||||
$counter = 1;
|
||||
|
||||
// 5 Pending Members
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$members[] = Member::create([
|
||||
'user_id' => $i === 0 ? $users['member']->id : null,
|
||||
'full_name' => "待審核會員 {$counter}",
|
||||
'email' => "pending{$counter}@test.com",
|
||||
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
|
||||
'address_line_1' => "測試地址 {$counter} 號",
|
||||
'city' => $taiwanCities[array_rand($taiwanCities)],
|
||||
'postal_code' => '100',
|
||||
'membership_status' => Member::STATUS_PENDING,
|
||||
'membership_type' => Member::TYPE_REGULAR,
|
||||
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
]);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
// 8 Active Members
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$startDate = now()->subMonths(rand(1, 6));
|
||||
$members[] = Member::create([
|
||||
'user_id' => null,
|
||||
'full_name' => "活躍會員 {$counter}",
|
||||
'email' => "active{$counter}@test.com",
|
||||
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
|
||||
'address_line_1' => "測試地址 {$counter} 號",
|
||||
'city' => $taiwanCities[array_rand($taiwanCities)],
|
||||
'postal_code' => '100',
|
||||
'membership_status' => Member::STATUS_ACTIVE,
|
||||
'membership_type' => $i < 6 ? Member::TYPE_REGULAR : ($i === 6 ? Member::TYPE_HONORARY : Member::TYPE_STUDENT),
|
||||
'membership_started_at' => $startDate,
|
||||
'membership_expires_at' => $startDate->copy()->addYear(),
|
||||
'emergency_contact_name' => "緊急聯絡人 {$counter}",
|
||||
'emergency_contact_phone' => '02-12345678',
|
||||
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
]);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
// 3 Expired Members
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$startDate = now()->subYears(2);
|
||||
$members[] = Member::create([
|
||||
'user_id' => null,
|
||||
'full_name' => "過期會員 {$counter}",
|
||||
'email' => "expired{$counter}@test.com",
|
||||
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
|
||||
'address_line_1' => "測試地址 {$counter} 號",
|
||||
'city' => $taiwanCities[array_rand($taiwanCities)],
|
||||
'postal_code' => '100',
|
||||
'membership_status' => Member::STATUS_EXPIRED,
|
||||
'membership_type' => Member::TYPE_REGULAR,
|
||||
'membership_started_at' => $startDate,
|
||||
'membership_expires_at' => $startDate->copy()->addYear(),
|
||||
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
]);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
// 2 Suspended Members
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$members[] = Member::create([
|
||||
'user_id' => null,
|
||||
'full_name' => "停權會員 {$counter}",
|
||||
'email' => "suspended{$counter}@test.com",
|
||||
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
|
||||
'address_line_1' => "測試地址 {$counter} 號",
|
||||
'city' => $taiwanCities[array_rand($taiwanCities)],
|
||||
'postal_code' => '100',
|
||||
'membership_status' => Member::STATUS_SUSPENDED,
|
||||
'membership_type' => Member::TYPE_REGULAR,
|
||||
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
|
||||
]);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
// 2 Additional Pending Members (total 20)
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$members[] = Member::create([
|
||||
'user_id' => null,
|
||||
'full_name' => "新申請會員 {$counter}",
|
||||
'email' => "newmember{$counter}@test.com",
|
||||
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
|
||||
'address_line_1' => "測試地址 {$counter} 號",
|
||||
'city' => $taiwanCities[array_rand($taiwanCities)],
|
||||
'postal_code' => '100',
|
||||
'membership_status' => Member::STATUS_PENDING,
|
||||
'membership_type' => Member::TYPE_REGULAR,
|
||||
]);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test membership payments at different approval stages
|
||||
*/
|
||||
private function createTestPayments(array $members, array $users): array
|
||||
{
|
||||
$payments = [];
|
||||
$paymentMethods = [
|
||||
MembershipPayment::METHOD_BANK_TRANSFER,
|
||||
MembershipPayment::METHOD_CASH,
|
||||
MembershipPayment::METHOD_CHECK,
|
||||
];
|
||||
|
||||
// 10 Pending Payments
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$payments[] = MembershipPayment::create([
|
||||
'member_id' => $members[$i]->id,
|
||||
'amount' => 1000,
|
||||
'paid_at' => now()->subDays(rand(1, 10)),
|
||||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||||
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
|
||||
'status' => MembershipPayment::STATUS_PENDING,
|
||||
'notes' => '待審核的繳費記錄',
|
||||
]);
|
||||
}
|
||||
|
||||
// 8 Approved by Cashier
|
||||
for ($i = 10; $i < 18; $i++) {
|
||||
$payments[] = MembershipPayment::create([
|
||||
'member_id' => $members[$i % count($members)]->id,
|
||||
'amount' => 1000,
|
||||
'paid_at' => now()->subDays(rand(5, 15)),
|
||||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||||
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
|
||||
'status' => MembershipPayment::STATUS_APPROVED_CASHIER,
|
||||
'verified_by_cashier_id' => $users['cashier']->id,
|
||||
'cashier_verified_at' => now()->subDays(rand(3, 12)),
|
||||
'cashier_notes' => '收據已核對,金額無誤',
|
||||
'notes' => '已通過出納審核',
|
||||
]);
|
||||
}
|
||||
|
||||
// 6 Approved by Accountant
|
||||
for ($i = 18; $i < 24; $i++) {
|
||||
$cashierVerifiedAt = now()->subDays(rand(10, 20));
|
||||
$accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3));
|
||||
|
||||
$payments[] = MembershipPayment::create([
|
||||
'member_id' => $members[$i % count($members)]->id,
|
||||
'amount' => 1000,
|
||||
'paid_at' => now()->subDays(rand(15, 25)),
|
||||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||||
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
|
||||
'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT,
|
||||
'verified_by_cashier_id' => $users['cashier']->id,
|
||||
'cashier_verified_at' => $cashierVerifiedAt,
|
||||
'cashier_notes' => '收據已核對,金額無誤',
|
||||
'verified_by_accountant_id' => $users['accountant']->id,
|
||||
'accountant_verified_at' => $accountantVerifiedAt,
|
||||
'accountant_notes' => '帳務核對完成',
|
||||
'notes' => '已通過會計審核',
|
||||
]);
|
||||
}
|
||||
|
||||
// 4 Fully Approved (Chair approved - member activated)
|
||||
for ($i = 24; $i < 28; $i++) {
|
||||
$cashierVerifiedAt = now()->subDays(rand(20, 30));
|
||||
$accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3));
|
||||
$chairVerifiedAt = $accountantVerifiedAt->copy()->addDays(rand(1, 2));
|
||||
|
||||
$payments[] = MembershipPayment::create([
|
||||
'member_id' => $members[$i % count($members)]->id,
|
||||
'amount' => 1000,
|
||||
'paid_at' => now()->subDays(rand(25, 35)),
|
||||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||||
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
|
||||
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
|
||||
'verified_by_cashier_id' => $users['cashier']->id,
|
||||
'cashier_verified_at' => $cashierVerifiedAt,
|
||||
'cashier_notes' => '收據已核對,金額無誤',
|
||||
'verified_by_accountant_id' => $users['accountant']->id,
|
||||
'accountant_verified_at' => $accountantVerifiedAt,
|
||||
'accountant_notes' => '帳務核對完成',
|
||||
'verified_by_chair_id' => $users['chair']->id,
|
||||
'chair_verified_at' => $chairVerifiedAt,
|
||||
'chair_notes' => '最終批准',
|
||||
'notes' => '已完成三階段審核',
|
||||
]);
|
||||
}
|
||||
|
||||
// 2 Rejected Payments
|
||||
for ($i = 28; $i < 30; $i++) {
|
||||
$payments[] = MembershipPayment::create([
|
||||
'member_id' => $members[$i % count($members)]->id,
|
||||
'amount' => 1000,
|
||||
'paid_at' => now()->subDays(rand(5, 10)),
|
||||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||||
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
|
||||
'status' => MembershipPayment::STATUS_REJECTED,
|
||||
'rejected_by_user_id' => $users['cashier']->id,
|
||||
'rejected_at' => now()->subDays(rand(3, 8)),
|
||||
'rejection_reason' => $i === 28 ? '收據影像不清晰,無法辨識' : '金額與收據不符',
|
||||
'notes' => '已退回',
|
||||
]);
|
||||
}
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test issues with various statuses
|
||||
*/
|
||||
private function createTestIssues(array $users, array $members): array
|
||||
{
|
||||
$issues = [];
|
||||
$labels = IssueLabel::all();
|
||||
|
||||
// 5 New/Open Issues
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$issue = Issue::create([
|
||||
'title' => "新任務:測試項目 " . ($i + 1),
|
||||
'description' => "這是一個新的測試任務,用於系統測試。\n\n## 任務說明\n- 測試項目 A\n- 測試項目 B\n- 測試項目 C",
|
||||
'issue_type' => [Issue::TYPE_WORK_ITEM, Issue::TYPE_PROJECT_TASK][array_rand([0, 1])],
|
||||
'status' => Issue::STATUS_NEW,
|
||||
'priority' => [Issue::PRIORITY_LOW, Issue::PRIORITY_MEDIUM, Issue::PRIORITY_HIGH][array_rand([0, 1, 2])],
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'due_date' => now()->addDays(rand(7, 30)),
|
||||
'estimated_hours' => rand(2, 16),
|
||||
]);
|
||||
|
||||
// Add labels
|
||||
if ($labels->count() > 0) {
|
||||
$issue->labels()->attach($labels->random(rand(1, min(3, $labels->count()))));
|
||||
}
|
||||
|
||||
$issues[] = $issue;
|
||||
}
|
||||
|
||||
// 4 In Progress Issues
|
||||
for ($i = 5; $i < 9; $i++) {
|
||||
$issue = Issue::create([
|
||||
'title' => "進行中:開發任務 " . ($i + 1),
|
||||
'description' => "這是一個進行中的開發任務。\n\n## 進度\n- [x] 需求分析\n- [x] 設計\n- [ ] 實作\n- [ ] 測試",
|
||||
'issue_type' => Issue::TYPE_WORK_ITEM,
|
||||
'status' => Issue::STATUS_IN_PROGRESS,
|
||||
'priority' => [Issue::PRIORITY_MEDIUM, Issue::PRIORITY_HIGH][array_rand([0, 1])],
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'assigned_to_user_id' => $users['member']->id,
|
||||
'due_date' => now()->addDays(rand(3, 14)),
|
||||
'estimated_hours' => rand(4, 20),
|
||||
]);
|
||||
|
||||
// Add time logs
|
||||
IssueTimeLog::create([
|
||||
'issue_id' => $issue->id,
|
||||
'user_id' => $users['member']->id,
|
||||
'hours' => rand(1, 5),
|
||||
'description' => '開發進度更新',
|
||||
'logged_at' => now()->subDays(rand(1, 3)),
|
||||
]);
|
||||
|
||||
// Add comments
|
||||
IssueComment::create([
|
||||
'issue_id' => $issue->id,
|
||||
'user_id' => $users['admin']->id,
|
||||
'comment' => '請加快進度,謝謝!',
|
||||
'created_at' => now()->subDays(rand(1, 2)),
|
||||
]);
|
||||
|
||||
if ($labels->count() > 0) {
|
||||
$issue->labels()->attach($labels->random(rand(1, min(2, $labels->count()))));
|
||||
}
|
||||
|
||||
$issues[] = $issue;
|
||||
}
|
||||
|
||||
// 3 Resolved Issues (in review)
|
||||
for ($i = 9; $i < 12; $i++) {
|
||||
$issue = Issue::create([
|
||||
'title' => "已完成:維護項目 " . ($i + 1),
|
||||
'description' => "維護任務已完成,等待審核。",
|
||||
'issue_type' => Issue::TYPE_MAINTENANCE,
|
||||
'status' => Issue::STATUS_REVIEW,
|
||||
'priority' => Issue::PRIORITY_MEDIUM,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'assigned_to_user_id' => $users['member']->id,
|
||||
'reviewer_id' => $users['manager']->id,
|
||||
'due_date' => now()->subDays(rand(1, 5)),
|
||||
'estimated_hours' => rand(2, 8),
|
||||
]);
|
||||
|
||||
// Add completed time logs
|
||||
IssueTimeLog::create([
|
||||
'issue_id' => $issue->id,
|
||||
'user_id' => $users['member']->id,
|
||||
'hours' => rand(2, 6),
|
||||
'description' => '任務完成',
|
||||
'logged_at' => now()->subDays(rand(1, 3)),
|
||||
]);
|
||||
|
||||
IssueComment::create([
|
||||
'issue_id' => $issue->id,
|
||||
'user_id' => $users['member']->id,
|
||||
'comment' => '任務已完成,請審核。',
|
||||
'created_at' => now()->subDays(1),
|
||||
]);
|
||||
|
||||
if ($labels->count() > 0) {
|
||||
$issue->labels()->attach($labels->random(1));
|
||||
}
|
||||
|
||||
$issues[] = $issue;
|
||||
}
|
||||
|
||||
// 2 Closed Issues
|
||||
for ($i = 12; $i < 14; $i++) {
|
||||
$closedAt = now()->subDays(rand(7, 30));
|
||||
$issue = Issue::create([
|
||||
'title' => "已結案:專案 " . ($i + 1),
|
||||
'description' => "專案已完成並結案。",
|
||||
'issue_type' => Issue::TYPE_PROJECT_TASK,
|
||||
'status' => Issue::STATUS_CLOSED,
|
||||
'priority' => Issue::PRIORITY_HIGH,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'assigned_to_user_id' => $users['member']->id,
|
||||
'due_date' => $closedAt->copy()->subDays(rand(1, 5)),
|
||||
'closed_at' => $closedAt,
|
||||
'estimated_hours' => rand(8, 24),
|
||||
]);
|
||||
|
||||
IssueTimeLog::create([
|
||||
'issue_id' => $issue->id,
|
||||
'user_id' => $users['member']->id,
|
||||
'hours' => rand(8, 20),
|
||||
'description' => '專案完成',
|
||||
'logged_at' => $closedAt->copy()->subDays(1),
|
||||
]);
|
||||
|
||||
IssueComment::create([
|
||||
'issue_id' => $issue->id,
|
||||
'user_id' => $users['admin']->id,
|
||||
'comment' => '專案驗收通過,結案。',
|
||||
'created_at' => $closedAt,
|
||||
]);
|
||||
|
||||
if ($labels->count() > 0) {
|
||||
$issue->labels()->attach($labels->random(rand(1, min(2, $labels->count()))));
|
||||
}
|
||||
|
||||
$issues[] = $issue;
|
||||
}
|
||||
|
||||
// 1 Member Request
|
||||
$issue = Issue::create([
|
||||
'title' => "會員需求:更新會員資料",
|
||||
'description' => "會員要求更新個人資料。",
|
||||
'issue_type' => Issue::TYPE_MEMBER_REQUEST,
|
||||
'status' => Issue::STATUS_ASSIGNED,
|
||||
'priority' => Issue::PRIORITY_MEDIUM,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'assigned_to_user_id' => $users['manager']->id,
|
||||
'member_id' => $members[0]->id,
|
||||
'due_date' => now()->addDays(3),
|
||||
'estimated_hours' => 1,
|
||||
]);
|
||||
|
||||
if ($labels->count() > 0) {
|
||||
$issue->labels()->attach($labels->random(1));
|
||||
}
|
||||
|
||||
$issues[] = $issue;
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test budgets with items
|
||||
*/
|
||||
private function createTestBudgets(array $users): array
|
||||
{
|
||||
$budgets = [];
|
||||
$accounts = ChartOfAccount::all();
|
||||
|
||||
if ($accounts->isEmpty()) {
|
||||
$this->command->warn('⚠️ No Chart of Accounts found. Skipping budget creation.');
|
||||
return $budgets;
|
||||
}
|
||||
|
||||
$incomeAccounts = $accounts->where('account_type', ChartOfAccount::TYPE_INCOME);
|
||||
$expenseAccounts = $accounts->where('account_type', ChartOfAccount::TYPE_EXPENSE);
|
||||
|
||||
// 1. Draft Budget
|
||||
$budget = Budget::create([
|
||||
'name' => '2025年度預算草案',
|
||||
'fiscal_year' => 2025,
|
||||
'status' => Budget::STATUS_DRAFT,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'notes' => '年度預算初稿,待提交',
|
||||
]);
|
||||
|
||||
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 50000, 40000);
|
||||
$budgets[] = $budget;
|
||||
|
||||
// 2. Submitted Budget
|
||||
$budget = Budget::create([
|
||||
'name' => '2024下半年預算',
|
||||
'fiscal_year' => 2024,
|
||||
'status' => Budget::STATUS_SUBMITTED,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'submitted_at' => now()->subDays(5),
|
||||
'notes' => '已提交,等待審核',
|
||||
]);
|
||||
|
||||
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 60000, 50000);
|
||||
$budgets[] = $budget;
|
||||
|
||||
// 3. Approved Budget
|
||||
$budget = Budget::create([
|
||||
'name' => '2024上半年預算',
|
||||
'fiscal_year' => 2024,
|
||||
'status' => Budget::STATUS_APPROVED,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'submitted_at' => now()->subDays(60),
|
||||
'approved_by_user_id' => $users['chair']->id,
|
||||
'approved_at' => now()->subDays(55),
|
||||
'notes' => '已核准',
|
||||
]);
|
||||
|
||||
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 70000, 60000);
|
||||
$budgets[] = $budget;
|
||||
|
||||
// 4. Active Budget
|
||||
$budget = Budget::create([
|
||||
'name' => '2024年度預算',
|
||||
'fiscal_year' => 2024,
|
||||
'status' => Budget::STATUS_ACTIVE,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'submitted_at' => now()->subMonths(11),
|
||||
'approved_by_user_id' => $users['chair']->id,
|
||||
'approved_at' => now()->subMonths(10),
|
||||
'activated_at' => now()->subMonths(10),
|
||||
'notes' => '執行中的年度預算',
|
||||
]);
|
||||
|
||||
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 100000, 80000, true);
|
||||
$budgets[] = $budget;
|
||||
|
||||
// 5. Closed Budget
|
||||
$budget = Budget::create([
|
||||
'name' => '2023年度預算',
|
||||
'fiscal_year' => 2023,
|
||||
'status' => Budget::STATUS_CLOSED,
|
||||
'created_by_user_id' => $users['admin']->id,
|
||||
'submitted_at' => now()->subMonths(23),
|
||||
'approved_by_user_id' => $users['chair']->id,
|
||||
'approved_at' => now()->subMonths(22),
|
||||
'activated_at' => now()->subMonths(22),
|
||||
'closed_at' => now()->subMonths(10),
|
||||
'notes' => '已結案的年度預算',
|
||||
]);
|
||||
|
||||
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 90000, 75000, true);
|
||||
$budgets[] = $budget;
|
||||
|
||||
return $budgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create budget items for a budget
|
||||
*/
|
||||
private function createBudgetItems(
|
||||
Budget $budget,
|
||||
$incomeAccounts,
|
||||
$expenseAccounts,
|
||||
int $totalIncome,
|
||||
int $totalExpense,
|
||||
bool $withActuals = false
|
||||
): void {
|
||||
// Create income items
|
||||
if ($incomeAccounts->count() > 0) {
|
||||
$itemCount = min(3, $incomeAccounts->count());
|
||||
$accounts = $incomeAccounts->random($itemCount);
|
||||
|
||||
foreach ($accounts as $index => $account) {
|
||||
$budgetedAmount = (int)($totalIncome / $itemCount);
|
||||
$actualAmount = $withActuals ? (int)($budgetedAmount * rand(80, 120) / 100) : 0;
|
||||
|
||||
BudgetItem::create([
|
||||
'budget_id' => $budget->id,
|
||||
'chart_of_account_id' => $account->id,
|
||||
'budgeted_amount' => $budgetedAmount,
|
||||
'actual_amount' => $actualAmount,
|
||||
'notes' => '預算項目',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create expense items
|
||||
if ($expenseAccounts->count() > 0) {
|
||||
$itemCount = min(5, $expenseAccounts->count());
|
||||
$accounts = $expenseAccounts->random($itemCount);
|
||||
|
||||
foreach ($accounts as $index => $account) {
|
||||
$budgetedAmount = (int)($totalExpense / $itemCount);
|
||||
$actualAmount = $withActuals ? (int)($budgetedAmount * rand(70, 110) / 100) : 0;
|
||||
|
||||
BudgetItem::create([
|
||||
'budget_id' => $budget->id,
|
||||
'chart_of_account_id' => $account->id,
|
||||
'budgeted_amount' => $budgetedAmount,
|
||||
'actual_amount' => $actualAmount,
|
||||
'notes' => '支出預算項目',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test finance documents
|
||||
*/
|
||||
private function createTestFinanceDocuments(array $users): array
|
||||
{
|
||||
$documents = [];
|
||||
|
||||
$documentTypes = ['invoice', 'receipt', 'contract', 'report'];
|
||||
$statuses = ['pending', 'approved', 'rejected'];
|
||||
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$documents[] = FinanceDocument::create([
|
||||
'document_number' => 'FIN-2024-' . str_pad($i, 4, '0', STR_PAD_LEFT),
|
||||
'title' => "財務文件 {$i}",
|
||||
'document_type' => $documentTypes[array_rand($documentTypes)],
|
||||
'amount' => rand(1000, 50000),
|
||||
'document_date' => now()->subDays(rand(1, 90)),
|
||||
'status' => $statuses[array_rand($statuses)],
|
||||
'uploaded_by_user_id' => $users['admin']->id,
|
||||
'file_path' => "finance-documents/test-doc-{$i}.pdf",
|
||||
'notes' => '測試財務文件',
|
||||
]);
|
||||
}
|
||||
|
||||
return $documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sample transactions
|
||||
*/
|
||||
private function createTestTransactions(array $users): array
|
||||
{
|
||||
$transactions = [];
|
||||
$accounts = ChartOfAccount::all();
|
||||
|
||||
if ($accounts->isEmpty()) {
|
||||
$this->command->warn('⚠️ No Chart of Accounts found. Skipping transaction creation.');
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
// Create 20 sample transactions
|
||||
for ($i = 1; $i <= 20; $i++) {
|
||||
$account = $accounts->random();
|
||||
$isDebit = $account->account_type === ChartOfAccount::TYPE_EXPENSE ||
|
||||
$account->account_type === ChartOfAccount::TYPE_ASSET;
|
||||
|
||||
$transactions[] = Transaction::create([
|
||||
'transaction_date' => now()->subDays(rand(1, 60)),
|
||||
'chart_of_account_id' => $account->id,
|
||||
'description' => "測試交易 {$i}:" . $account->account_name,
|
||||
'debit_amount' => $isDebit ? rand(500, 10000) : 0,
|
||||
'credit_amount' => !$isDebit ? rand(500, 10000) : 0,
|
||||
'created_by_user_id' => $users['accountant']->id,
|
||||
'reference' => 'TXN-' . str_pad($i, 5, '0', STR_PAD_LEFT),
|
||||
'notes' => '系統測試交易',
|
||||
]);
|
||||
}
|
||||
|
||||
return $transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display test account information
|
||||
*/
|
||||
private function displayTestAccounts(array $users): void
|
||||
{
|
||||
$this->command->info('📋 Test User Accounts:');
|
||||
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
$this->command->table(
|
||||
['Role', 'Email', 'Password', 'Permissions'],
|
||||
[
|
||||
['Admin', 'admin@test.com', 'password', 'All permissions'],
|
||||
['Cashier', 'cashier@test.com', 'password', 'Tier 1 payment verification'],
|
||||
['Accountant', 'accountant@test.com', 'password', 'Tier 2 payment verification'],
|
||||
['Chair', 'chair@test.com', 'password', 'Tier 3 payment verification'],
|
||||
['Manager', 'manager@test.com', 'password', 'Membership activation'],
|
||||
['Member', 'member@test.com', 'password', 'Member dashboard access'],
|
||||
]
|
||||
);
|
||||
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
$this->command->info('');
|
||||
$this->command->info('🎯 Test Data Summary:');
|
||||
$this->command->info(' • 20 Members (5 pending, 8 active, 3 expired, 2 suspended, 2 new)');
|
||||
$this->command->info(' • 30 Payments (10 pending, 8 cashier-approved, 6 accountant-approved, 4 fully-approved, 2 rejected)');
|
||||
$this->command->info(' • 15 Issues (5 new, 4 in-progress, 3 in-review, 2 closed, 1 member-request)');
|
||||
$this->command->info(' • 5 Budgets (draft, submitted, approved, active, closed)');
|
||||
$this->command->info(' • 10 Finance Documents');
|
||||
$this->command->info(' • 20 Sample Transactions');
|
||||
$this->command->info('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user