feat: 租戶建立自動產生預設網域與管理員帳號
- 修改 TenantController 自動產生預設網域 ({tenant_id}.{TENANT_DEFAULT_DOMAIN})
- 新增 TenantDatabaseSeeder 自動建立 admin 帳號
- 啟用 SeedDatabase Job 在建立租戶時自動執行 seeder
- 新增 TENANT_DEFAULT_DOMAIN 環境變數支援不同環境
- 補充中央資料庫所需的 migrations
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
APP_NAME=KooriERP
|
APP_NAME=StarERP
|
||||||
COMPOSE_PROJECT_NAME=koori-erp
|
COMPOSE_PROJECT_NAME=star-erp
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
@@ -7,6 +7,7 @@ APP_URL=http://localhost
|
|||||||
|
|
||||||
# Multi-tenancy 設定 (用逗號分隔多個中央網域)
|
# Multi-tenancy 設定 (用逗號分隔多個中央網域)
|
||||||
CENTRAL_DOMAINS=localhost,127.0.0.1
|
CENTRAL_DOMAINS=localhost,127.0.0.1
|
||||||
|
TENANT_DEFAULT_DOMAIN=star-erp.test
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
@@ -27,7 +28,7 @@ LOG_LEVEL=debug
|
|||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=mysql
|
DB_HOST=mysql
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=koori_erp
|
DB_DATABASE=star_erp
|
||||||
DB_USERNAME=sail
|
DB_USERNAME=sail
|
||||||
DB_PASSWORD=password
|
DB_PASSWORD=password
|
||||||
FORWARD_DB_PORT=3307
|
FORWARD_DB_PORT=3307
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Koori ERP
|
# Star ERP
|
||||||
|
|
||||||
本專案是一個基於 Laravel 12, Inertia.js (React) 與 Tailwind CSS 開發的 ERP 系統。
|
本專案是一個基於 Laravel 12, Inertia.js (React) 與 Tailwind CSS 開發的 ERP 系統。
|
||||||
|
|
||||||
@@ -36,25 +36,25 @@ cp .env.example .env
|
|||||||
# 背景執行容器
|
# 背景執行容器
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
docker exec -it koori-erp-laravel.test-1 composer install
|
docker exec -it star-erp-laravel composer install
|
||||||
|
|
||||||
# 生成 App Key
|
# 生成 App Key
|
||||||
docker exec -it koori-erp-laravel.test-1 php artisan key:generate
|
docker exec -it star-erp-laravel php artisan key:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 資料庫遷移與初始化
|
### 3. 資料庫遷移與初始化
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# (選填) 如果有種子資料
|
# (選填) 如果有種子資料
|
||||||
docker exec -it koori-erp-laravel.test-1 php artisan migrate --seed
|
docker exec -it star-erp-laravel php artisan migrate --seed
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 啟動前端開發伺服器 (Vite)
|
### 4. 啟動前端開發伺服器 (Vite)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it koori-erp-laravel npm install
|
docker exec -it star-erp-laravel npm install
|
||||||
docker exec -it koori-erp-laravel npm run dev
|
docker exec -it star-erp-laravel npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
啟動後,您可以透過以下連結瀏覽專案:
|
啟動後,您可以透過以下連結瀏覽專案:
|
||||||
|
|||||||
@@ -58,10 +58,12 @@ class TenantController extends Controller
|
|||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 如果有指定域名,則綁定
|
// 綁定網域(如果沒有輸入,使用預設網域)
|
||||||
if (!empty($validated['domain'])) {
|
$defaultDomain = env('TENANT_DEFAULT_DOMAIN', 'star-erp.test');
|
||||||
$tenant->domains()->create(['domain' => $validated['domain']]);
|
$domain = !empty($validated['domain'])
|
||||||
}
|
? $validated['domain']
|
||||||
|
: $validated['id'] . '.' . $defaultDomain;
|
||||||
|
$tenant->domains()->create(['domain' => $domain]);
|
||||||
|
|
||||||
return redirect()->route('landlord.tenants.index')
|
return redirect()->route('landlord.tenants.index')
|
||||||
->with('success', "租戶 {$validated['name']} 建立成功!");
|
->with('success', "租戶 {$validated['name']} 建立成功!");
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||||||
JobPipeline::make([
|
JobPipeline::make([
|
||||||
Jobs\CreateDatabase::class,
|
Jobs\CreateDatabase::class,
|
||||||
Jobs\MigrateDatabase::class,
|
Jobs\MigrateDatabase::class,
|
||||||
// Jobs\SeedDatabase::class,
|
Jobs\SeedDatabase::class,
|
||||||
|
|
||||||
// Your own jobs to prepare the tenant.
|
// Your own jobs to prepare the tenant.
|
||||||
// Provision API keys, create S3 buckets, anything you want!
|
// Provision API keys, create S3 buckets, anything you want!
|
||||||
|
|||||||
12
compose.yaml
12
compose.yaml
@@ -6,8 +6,8 @@ services:
|
|||||||
args:
|
args:
|
||||||
WWWGROUP: '${WWWGROUP}'
|
WWWGROUP: '${WWWGROUP}'
|
||||||
image: 'sail-8.5/app'
|
image: 'sail-8.5/app'
|
||||||
container_name: koori-erp-laravel
|
container_name: star-erp-laravel
|
||||||
hostname: koori-erp-laravel
|
hostname: star-erp-laravel
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
ports:
|
ports:
|
||||||
@@ -29,8 +29,8 @@ services:
|
|||||||
# - mailpit
|
# - mailpit
|
||||||
mysql:
|
mysql:
|
||||||
image: 'mysql/mysql-server:8.0'
|
image: 'mysql/mysql-server:8.0'
|
||||||
container_name: koori-erp-mysql
|
container_name: star-erp-mysql
|
||||||
hostname: koori-erp-mysql
|
hostname: star-erp-mysql
|
||||||
ports:
|
ports:
|
||||||
- '${FORWARD_DB_PORT:-3306}:3306'
|
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||||
environment:
|
environment:
|
||||||
@@ -56,8 +56,8 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
redis:
|
redis:
|
||||||
image: 'redis:alpine'
|
image: 'redis:alpine'
|
||||||
container_name: koori-erp-redis
|
container_name: star-erp-redis
|
||||||
hostname: koori-erp-redis
|
hostname: star-erp-redis
|
||||||
# ports:
|
# ports:
|
||||||
# - '${FORWARD_REDIS_PORT:-6379}:6379'
|
# - '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ return [
|
|||||||
* Parameters used by the tenants:seed command.
|
* Parameters used by the tenants:seed command.
|
||||||
*/
|
*/
|
||||||
'seeder_parameters' => [
|
'seeder_parameters' => [
|
||||||
'--class' => 'DatabaseSeeder', // root seeder class
|
'--class' => 'TenantDatabaseSeeder', // 租戶專用 seeder
|
||||||
// '--force' => true, // This needs to be true to seed tenant databases in production
|
// '--force' => true, // This needs to be true to seed tenant databases in production
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?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('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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('users', function (Blueprint $table) {
|
||||||
|
$table->string('username')->unique()->after('name');
|
||||||
|
$table->string('email')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('username');
|
||||||
|
$table->string('email')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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,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::table('roles', function (Blueprint $table) {
|
||||||
|
$table->string('display_name')->nullable()->after('name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('roles', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('display_name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$roles = [
|
||||||
|
'super-admin' => '系統管理員',
|
||||||
|
'admin' => '一般管理員',
|
||||||
|
'warehouse-manager' => '倉庫管理員',
|
||||||
|
'purchaser' => '採購人員',
|
||||||
|
'viewer' => '檢視人員',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($roles as $name => $displayName) {
|
||||||
|
DB::table('roles')
|
||||||
|
->where('name', $name)
|
||||||
|
->update(['display_name' => $displayName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$roles = [
|
||||||
|
'super-admin',
|
||||||
|
'admin',
|
||||||
|
'warehouse-manager',
|
||||||
|
'purchaser',
|
||||||
|
'viewer',
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::table('roles')
|
||||||
|
->whereIn('name', $roles)
|
||||||
|
->update(['display_name' => null]);
|
||||||
|
}
|
||||||
|
};
|
||||||
42
database/seeders/TenantDatabaseSeeder.php
Normal file
42
database/seeders/TenantDatabaseSeeder.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\User;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租戶資料庫專用 Seeder
|
||||||
|
*
|
||||||
|
* 建立新租戶時會自動執行此 Seeder,負責:
|
||||||
|
* 1. 建立預設的超級管理員帳號
|
||||||
|
* 2. 設定權限與角色
|
||||||
|
*/
|
||||||
|
class TenantDatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// 建立預設管理員帳號
|
||||||
|
$admin = User::firstOrCreate(
|
||||||
|
['username' => 'admin'],
|
||||||
|
[
|
||||||
|
'name' => '系統管理員',
|
||||||
|
'email' => 'admin@example.com',
|
||||||
|
'password' => 'password',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 呼叫權限 Seeder 設定權限與角色
|
||||||
|
$this->call(PermissionSeeder::class);
|
||||||
|
|
||||||
|
// 確保 admin 擁有 super-admin 角色
|
||||||
|
if (!$admin->hasRole('super-admin')) {
|
||||||
|
$admin->assignRole('super-admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "star-erp",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "star-erp",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/react": "^2.3.4",
|
"@inertiajs/react": "^2.3.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://www.schemastore.org/package.json",
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"name": "star-erp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function LandlordLogin() {
|
|||||||
<div className="w-full max-w-md p-8 relative z-10">
|
<div className="w-full max-w-md p-8 relative z-10">
|
||||||
<div className="flex flex-col items-center mb-8">
|
<div className="flex flex-col items-center mb-8">
|
||||||
{/* 使用不同風格的 Logo 或純文字 */}
|
{/* 使用不同風格的 Logo 或純文字 */}
|
||||||
<div className="text-white text-3xl font-bold tracking-wider mb-2">Koori ERP</div>
|
<div className="text-white text-3xl font-bold tracking-wider mb-2">Star ERP</div>
|
||||||
<div className="text-slate-400 text-sm tracking-widest uppercase">Central Administration</div>
|
<div className="text-slate-400 text-sm tracking-widest uppercase">Central Administration</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default function Welcome() {
|
|||||||
<Head title="Welcome" />
|
<Head title="Welcome" />
|
||||||
<div className="p-8 bg-white rounded-lg shadow-lg">
|
<div className="p-8 bg-white rounded-lg shadow-lg">
|
||||||
<h1 className="text-4xl font-bold text-blue-600">
|
<h1 className="text-4xl font-bold text-blue-600">
|
||||||
Koori ERP
|
Star ERP
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-gray-600">
|
<p className="mt-4 text-gray-600">
|
||||||
React + Inertia + Laravel Integration Successful!
|
React + Inertia + Laravel Integration Successful!
|
||||||
|
|||||||
Reference in New Issue
Block a user