diff --git a/.env.example b/.env.example index a4eae50..b00cef0 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -APP_NAME=KooriERP -COMPOSE_PROJECT_NAME=koori-erp +APP_NAME=StarERP +COMPOSE_PROJECT_NAME=star-erp APP_ENV=local APP_KEY= APP_DEBUG=true @@ -7,6 +7,7 @@ APP_URL=http://localhost # Multi-tenancy 設定 (用逗號分隔多個中央網域) CENTRAL_DOMAINS=localhost,127.0.0.1 +TENANT_DEFAULT_DOMAIN=star-erp.test APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -27,7 +28,7 @@ LOG_LEVEL=debug DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 -DB_DATABASE=koori_erp +DB_DATABASE=star_erp DB_USERNAME=sail DB_PASSWORD=password FORWARD_DB_PORT=3307 diff --git a/README.md b/README.md index 5f91901..3794aa8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Koori ERP +# Star ERP 本專案是一個基於 Laravel 12, Inertia.js (React) 與 Tailwind CSS 開發的 ERP 系統。 @@ -36,25 +36,25 @@ cp .env.example .env # 背景執行容器 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 -docker exec -it koori-erp-laravel.test-1 php artisan key:generate +docker exec -it star-erp-laravel php artisan key:generate ``` ### 3. 資料庫遷移與初始化 ```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) ```bash -docker exec -it koori-erp-laravel npm install -docker exec -it koori-erp-laravel npm run dev +docker exec -it star-erp-laravel npm install +docker exec -it star-erp-laravel npm run dev ``` 啟動後,您可以透過以下連結瀏覽專案: diff --git a/app/Http/Controllers/Landlord/TenantController.php b/app/Http/Controllers/Landlord/TenantController.php index 98ad551..cc24ad5 100644 --- a/app/Http/Controllers/Landlord/TenantController.php +++ b/app/Http/Controllers/Landlord/TenantController.php @@ -58,10 +58,12 @@ class TenantController extends Controller 'is_active' => true, ]); - // 如果有指定域名,則綁定 - if (!empty($validated['domain'])) { - $tenant->domains()->create(['domain' => $validated['domain']]); - } + // 綁定網域(如果沒有輸入,使用預設網域) + $defaultDomain = env('TENANT_DEFAULT_DOMAIN', 'star-erp.test'); + $domain = !empty($validated['domain']) + ? $validated['domain'] + : $validated['id'] . '.' . $defaultDomain; + $tenant->domains()->create(['domain' => $domain]); return redirect()->route('landlord.tenants.index') ->with('success', "租戶 {$validated['name']} 建立成功!"); diff --git a/app/Providers/TenancyServiceProvider.php b/app/Providers/TenancyServiceProvider.php index d94703c..e40c09d 100644 --- a/app/Providers/TenancyServiceProvider.php +++ b/app/Providers/TenancyServiceProvider.php @@ -27,7 +27,7 @@ class TenancyServiceProvider extends ServiceProvider JobPipeline::make([ Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, - // Jobs\SeedDatabase::class, + Jobs\SeedDatabase::class, // Your own jobs to prepare the tenant. // Provision API keys, create S3 buckets, anything you want! diff --git a/compose.yaml b/compose.yaml index 4f9cf72..27d9bcc 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,8 +6,8 @@ services: args: WWWGROUP: '${WWWGROUP}' image: 'sail-8.5/app' - container_name: koori-erp-laravel - hostname: koori-erp-laravel + container_name: star-erp-laravel + hostname: star-erp-laravel extra_hosts: - 'host.docker.internal:host-gateway' ports: @@ -29,8 +29,8 @@ services: # - mailpit mysql: image: 'mysql/mysql-server:8.0' - container_name: koori-erp-mysql - hostname: koori-erp-mysql + container_name: star-erp-mysql + hostname: star-erp-mysql ports: - '${FORWARD_DB_PORT:-3306}:3306' environment: @@ -56,8 +56,8 @@ services: timeout: 5s redis: image: 'redis:alpine' - container_name: koori-erp-redis - hostname: koori-erp-redis + container_name: star-erp-redis + hostname: star-erp-redis # ports: # - '${FORWARD_REDIS_PORT:-6379}:6379' volumes: diff --git a/config/tenancy.php b/config/tenancy.php index 7baf6ef..6bc7805 100644 --- a/config/tenancy.php +++ b/config/tenancy.php @@ -192,7 +192,7 @@ return [ * Parameters used by the tenants:seed command. */ 'seeder_parameters' => [ - '--class' => 'DatabaseSeeder', // root seeder class + '--class' => 'TenantDatabaseSeeder', // 租戶專用 seeder // '--force' => true, // This needs to be true to seed tenant databases in production ], ]; diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +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'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +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'); + } +}; diff --git a/database/migrations/2026_01_07_132554_add_username_to_users_table.php b/database/migrations/2026_01_07_132554_add_username_to_users_table.php new file mode 100644 index 0000000..2b9a7ab --- /dev/null +++ b/database/migrations/2026_01_07_132554_add_username_to_users_table.php @@ -0,0 +1,30 @@ +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(); + }); + } +}; diff --git a/database/migrations/2026_01_13_113720_create_permission_tables.php b/database/migrations/2026_01_13_113720_create_permission_tables.php new file mode 100644 index 0000000..66ce1f9 --- /dev/null +++ b/database/migrations/2026_01_13_113720_create_permission_tables.php @@ -0,0 +1,134 @@ +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']); + } +}; diff --git a/database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php b/database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php new file mode 100644 index 0000000..b6f01ab --- /dev/null +++ b/database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php @@ -0,0 +1,28 @@ +string('display_name')->nullable()->after('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('display_name'); + }); + } +}; diff --git a/database/migrations/2026_01_14_090000_update_role_display_names.php b/database/migrations/2026_01_14_090000_update_role_display_names.php new file mode 100644 index 0000000..7118498 --- /dev/null +++ b/database/migrations/2026_01_14_090000_update_role_display_names.php @@ -0,0 +1,47 @@ + '系統管理員', + '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]); + } +}; diff --git a/database/seeders/TenantDatabaseSeeder.php b/database/seeders/TenantDatabaseSeeder.php new file mode 100644 index 0000000..7a86914 --- /dev/null +++ b/database/seeders/TenantDatabaseSeeder.php @@ -0,0 +1,42 @@ + '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'); + } + } +} diff --git a/package-lock.json b/package-lock.json index b11002c..8d3b5cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,10 @@ { - "name": "html", + "name": "star-erp", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "star-erp", "dependencies": { "@inertiajs/react": "^2.3.4", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/package.json b/package.json index 23bec74..c107222 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "$schema": "https://www.schemastore.org/package.json", + "name": "star-erp", "private": true, "type": "module", "scripts": { @@ -45,4 +46,4 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0" } -} +} \ No newline at end of file diff --git a/resources/js/Pages/Landlord/Auth/Login.tsx b/resources/js/Pages/Landlord/Auth/Login.tsx index 6d12337..caf3a4c 100644 --- a/resources/js/Pages/Landlord/Auth/Login.tsx +++ b/resources/js/Pages/Landlord/Auth/Login.tsx @@ -36,7 +36,7 @@ export default function LandlordLogin() {
{/* 使用不同風格的 Logo 或純文字 */} -
Koori ERP
+
Star ERP
Central Administration
diff --git a/resources/js/Pages/Welcome.tsx b/resources/js/Pages/Welcome.tsx index b690a52..0b13800 100644 --- a/resources/js/Pages/Welcome.tsx +++ b/resources/js/Pages/Welcome.tsx @@ -7,7 +7,7 @@ export default function Welcome() {

- Koori ERP + Star ERP

React + Inertia + Laravel Integration Successful!