diff --git a/.agent/rules/framework.md b/.agent/rules/framework.md index a3e5419..0b57d23 100644 --- a/.agent/rules/framework.md +++ b/.agent/rules/framework.md @@ -5,73 +5,64 @@ trigger: always_on --- trigger: always_on --- -預設專案運行於 WSL2 的 Laravel Sail (Docker) 環境。 -開發框架規範說明書:ERP 系統 (koori-erp) -1. 專案概述 -目標: 打造一個強大且穩定的 ERP 後台管理系統。 -核心架構: 採用 單體式架構配現代化前端 (Monolith with a Modern Frontend)。使用 Laravel、Inertia.js 及 React。 +# 開發框架規範說明書:ERP 系統 (star-erp) -工作流程: 將 UI/UX 設計師提供的 React 原始碼,透過 Inertia.js 整合進 Laravel 環境中。 +## 1. 專案概述 +* **目標**: 打造一個強大且穩定的 ERP 後台管理系統。 +* **核心架構**: 採用 **模組化單體式架構 (Modular Monolith)** 配現代化前端。使用 Laravel、Inertia.js 及 React。 +* **工作流程**: 將 UI/UX 設計師提供的 React 原始碼,透過 Inertia.js 整合進 Laravel 環境中。後端邏輯依據「業務領域」拆分為獨立模組。 -2. 技術棧 (Tech Stack) -後端: PHP 8.5 / Laravel 12 +## 2. 技術棧 (Tech Stack) +* **後端**: PHP 8.5 / Laravel 12 +* **前端橋樑**: Inertia.js (不使用傳統 RESTful API 串接頁面,改用 Inertia 協議) +* **前端庫**: React (以 Functional Components 與 Hooks 為主) +* **樣式處理**: Tailwind CSS (確保與 UI/UX 設計稿完全一致) +* **資料庫**: MySQL 8.0 +* **開發環境**: Laravel Sail (Docker / WSL2) +* **未來擴充**: 針對高併發或跨平台模組,預留 Golang 微服務接口。 -前端橋樑: Inertia.js (不使用傳統 RESTful API 串接頁面,改用 Inertia 協議) +## 3. 目錄結構與慣例 -前端庫: React (以 Functional Components 與 Hooks 為主) +### 3.1 後端 (Laravel - Modular Monolith) +系統採用模組化架構,核心邏輯位於 `app/Modules/` 下: -樣式處理: Tailwind CSS (確保與 UI/UX 設計稿完全一致) +* **Modules**: 位於 `app/Modules/{ModuleName}/`。 + * **Controllers**: `app/Modules/{ModuleName}/Controllers/`。必須回傳 `Inertia::render()`。 + * **Models**: `app/Modules/{ModuleName}/Models/`。 + * **Routes**: `app/Modules/{ModuleName}/Routes/web.php`。各模組獨立管理路由。 +* **Global Routes**: `routes/web.php` 僅保留全域通用路由或作為模組路由的載入點。 -資料庫: MySQL 8.0 +### 3.2 前端 (React) +* **Pages (頁面)**: 位於 `resources/js/Pages/`。每個檔案代表一個完整的路由視圖。 +* **Components (組件)**: 位於 `resources/js/Components/`。存放由 UI/UX 團隊提供的可重複使用 UI 元件。 +* **Layouts (版面)**: 位於 `resources/js/Layouts/`。定義 ERP 的通用版面。 -開發環境: Laravel Sail (Docker / WSL2) +## 4. 整合指南 (UI/UX 轉換至 Laravel) +* **組件遷移**: 將 UI/UX 的 React 原始碼移入 `resources/js/` 時,應進行「原子化」拆解,提高元件複用率。 +* **資料傳遞**: 透過 Laravel Controller 的 props 傳送動態資料給 React。優先使用 Inertia 資料流,避免初次渲染時使用 axios。 +* **狀態管理**: 優先使用 Inertia 內建的狀態管理與 useForm hook 處理表單提交。 -未來擴充: 針對高併發或跨平台模組,預留 Golang 微服務接口。 +## 5. 開發標準 (Coding Standards) +* **命名規範**: + * Controllers: `PascalCaseController.php` + * React Components: `PascalCase.jsx` + * Routes: `kebab-case` (小寫橫線分隔) +* **回傳格式**: 所有的前後端溝通需維持一致的 JSON 結構,特別是驗證錯誤 (Validation Errors) 與閃存訊息 (Flash Messages)。 -3. 目錄結構與慣例 -3.1 後端 (Laravel) -Controllers: 必須回傳 Inertia::render() 來渲染頁面。 +## 6. AI 協作規則 (給 Antigravity AI) +* **角色設定**: 你是一位專業的全端開發工程師助手。 +* **代碼生成指令**: + * 所有的解釋說明請使用 **繁體中文**。 + * 生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。 + * 必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。 + * 新增功能時,請先判斷應歸屬於哪個 Module,並建立在 `app/Modules/` 對應目錄下。 -Models: 嚴格執行型別標註,使用 Eloquent 進行資料庫操作。 +## 7. 運行機制 (Docker / Sail) +由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令: -Routes: 統一在 routes/web.php 定義 Inertia 路由。 - -3.2 前端 (React) -Pages (頁面): 位於 resources/js/Pages/。每個檔案代表一個完整的路由視圖。 - -Components (組件): 位於 resources/js/Components/。存放由 UI/UX 團隊提供的可重複使用 UI 元件。 - -Layouts (版面): 位於 resources/js/Layouts/。定義 ERP 的通用版面(例如:包含側邊欄 Sidebar 與導覽列 Navbar 的後台主框架)。 - -4. 整合指南 (UI/UX 轉換至 Laravel) -組件遷移: 將 UI/UX 的 React 原始碼移入 resources/js/ 時,應進行「原子化」拆解,提高元件複用率。 - -資料傳遞: 透過 Laravel Controller 的 props 傳送動態資料給 React。除非是後續的異步請求,否則避免在 React 初次渲染時使用 axios 抓取資料,應優先使用 Inertia 的資料流。 - -狀態管理: 優先使用 Inertia 內建的狀態管理與 useForm hook 處理表單提交。 - -5. 開發標準 (Coding Standards) -命名規範: - -Controllers: PascalCaseController.php - -React Components: PascalCase.jsx - -Routes: kebab-case (小寫橫線分隔) - -回傳格式: 所有的前後端溝通需維持一致的 JSON 結構,特別是驗證錯誤 (Validation Errors) 與閃存訊息 (Flash Messages)。 - -6. AI 協作規則 (給 Antigravity AI) -角色設定: 你是一位專業的全端開發工程師助手。 - -代碼生成指令: - -所有的解釋說明請使用 繁體中文。 - -生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。 - -必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。 - -7.運行機制 -因為是運行在docker上 所以要執行php的話 要執行docker exce \ No newline at end of file +* **啟動環境**: `./vendor/bin/sail up -d` +* **執行 PHP 指令**: `./vendor/bin/sail php -v` +* **執行 Artisan 指令**: `./vendor/bin/sail artisan route:list` +* **執行 Composer**: `./vendor/bin/sail composer install` +* **執行 Node/NPM**: `./vendor/bin/sail npm run dev` \ No newline at end of file diff --git a/.agent/skills/activity-logging/SKILL.md b/.agent/skills/activity-logging/SKILL.md index f8ec3ec..854df5a 100644 --- a/.agent/skills/activity-logging/SKILL.md +++ b/.agent/skills/activity-logging/SKILL.md @@ -101,8 +101,8 @@ public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, st protected function getSubjectMap() { return [ - 'App\Models\Product' => '商品', - 'App\Models\UtilityFee' => '公共事業費', // ✅ 新增映射 + 'App\Modules\Inventory\Models\Product' => '商品', + 'App\Modules\Finance\Models\UtilityFee' => '公共事業費', // ✅ 新增映射 ]; } ``` diff --git a/app/Console/Commands/MigrateToTenant.php b/app/Console/Commands/MigrateToTenant.php index 0f1f0fd..609b710 100644 --- a/app/Console/Commands/MigrateToTenant.php +++ b/app/Console/Commands/MigrateToTenant.php @@ -2,7 +2,7 @@ namespace App\Console\Commands; -use App\Models\Tenant; +use App\Modules\Core\Models\Tenant; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; diff --git a/app/Http/Controllers/Landlord/DashboardController.php b/app/Http/Controllers/Landlord/DashboardController.php index 82680d1..e7759cc 100644 --- a/app/Http/Controllers/Landlord/DashboardController.php +++ b/app/Http/Controllers/Landlord/DashboardController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers\Landlord; use App\Http\Controllers\Controller; -use App\Models\Tenant; +use App\Modules\Core\Models\Tenant; use Inertia\Inertia; class DashboardController extends Controller diff --git a/app/Http/Controllers/Landlord/TenantController.php b/app/Http/Controllers/Landlord/TenantController.php index 2e980b7..2f67647 100644 --- a/app/Http/Controllers/Landlord/TenantController.php +++ b/app/Http/Controllers/Landlord/TenantController.php @@ -3,7 +3,7 @@ namespace App\Http\Controllers\Landlord; use App\Http\Controllers\Controller; -use App\Models\Tenant; +use App\Modules\Core\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Validation\Rule; use Inertia\Inertia; diff --git a/app/Models/ProductionOrder.php b/app/Models/ProductionOrder.php deleted file mode 100644 index 81642a3..0000000 --- a/app/Models/ProductionOrder.php +++ /dev/null @@ -1,120 +0,0 @@ - 'date:Y-m-d', - 'expiry_date' => 'date:Y-m-d', - 'output_quantity' => 'decimal:2', - ]; - - /** - * 成品商品 - */ - public function product(): BelongsTo - { - return $this->belongsTo(Product::class); - } - - /** - * 入庫倉庫 - */ - public function warehouse(): BelongsTo - { - return $this->belongsTo(Warehouse::class); - } - - /** - * 操作人員 - */ - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - /** - * 生產工單明細 (BOM) - */ - public function items(): HasMany - { - return $this->hasMany(ProductionOrderItem::class); - } - - /** - * 活動日誌設定 - */ - public function getActivitylogOptions(): LogOptions - { - return LogOptions::defaults() - ->logAll() - ->logOnlyDirty() - ->dontSubmitEmptyLogs(); - } - - /** - * 活動日誌快照 - */ - public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) - { - $properties = $activity->properties; - $attributes = $properties['attributes'] ?? []; - $snapshot = $properties['snapshot'] ?? []; - - // 快照關鍵名稱 - $snapshot['production_code'] = $this->code; - $snapshot['product_name'] = $this->product ? $this->product->name : null; - $snapshot['warehouse_name'] = $this->warehouse ? $this->warehouse->name : null; - $snapshot['user_name'] = $this->user ? $this->user->name : null; - - $properties['attributes'] = $attributes; - $properties['snapshot'] = $snapshot; - $activity->properties = $properties; - } - - /** - * 產生生產單號 - */ - public static function generateCode(): string - { - $date = now()->format('Ymd'); - $prefix = "PRO-{$date}-"; - - $lastOrder = static::where('code', 'like', "{$prefix}%") - ->orderByDesc('code') - ->first(); - - if ($lastOrder) { - $lastNumber = (int) substr($lastOrder->code, -3); - $nextNumber = str_pad($lastNumber + 1, 3, '0', STR_PAD_LEFT); - } else { - $nextNumber = '001'; - } - - return $prefix . $nextNumber; - } -} diff --git a/app/Models/ProductionOrderItem.php b/app/Models/ProductionOrderItem.php deleted file mode 100644 index d8cf3fe..0000000 --- a/app/Models/ProductionOrderItem.php +++ /dev/null @@ -1,47 +0,0 @@ - 'decimal:4', - ]; - - /** - * 所屬生產工單 - */ - public function productionOrder(): BelongsTo - { - return $this->belongsTo(ProductionOrder::class); - } - - /** - * 使用的庫存紀錄 - */ - public function inventory(): BelongsTo - { - return $this->belongsTo(Inventory::class); - } - - /** - * 單位 - */ - public function unit(): BelongsTo - { - return $this->belongsTo(Unit::class); - } -} diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php deleted file mode 100644 index 794647e..0000000 --- a/app/Models/PurchaseOrder.php +++ /dev/null @@ -1,166 +0,0 @@ - 'date:Y-m-d', - 'invoice_date' => 'date:Y-m-d', - 'total_amount' => 'decimal:2', - 'tax_amount' => 'decimal:2', - 'grand_total' => 'decimal:2', - 'invoice_amount' => 'decimal:2', - ]; - - protected $appends = [ - 'poNumber', - 'supplierId', - 'supplierName', - 'expectedDate', - 'totalAmount', - 'taxAmount', // Add this - 'grandTotal', // Add this - 'createdBy', - 'warehouse_name', - 'createdAt', - 'invoiceNumber', - 'invoiceDate', - 'invoiceAmount', - ]; - - public function getCreatedAtAttribute() - { - return $this->attributes['created_at']; - } - - public function getPoNumberAttribute(): string - { - return $this->code; - } - - public function getSupplierIdAttribute(): string - { - return (string) $this->vendor_id; - } - - public function getSupplierNameAttribute(): string - { - return $this->vendor ? $this->vendor->name : ''; - } - - public function getExpectedDateAttribute(): ?string - { - return $this->attributes['expected_delivery_date'] ?? null; - } - - public function getTotalAmountAttribute(): float - { - return (float) ($this->attributes['total_amount'] ?? 0); - } - - public function getTaxAmountAttribute(): float - { - return (float) ($this->attributes['tax_amount'] ?? 0); - } - - public function getGrandTotalAttribute(): float - { - return (float) ($this->attributes['grand_total'] ?? 0); - } - - public function getCreatedByAttribute(): string - { - return $this->user ? $this->user->name : '系統'; - } - - public function getWarehouseNameAttribute(): string - { - return $this->warehouse ? $this->warehouse->name : ''; - } - - public function getInvoiceNumberAttribute(): ?string - { - return $this->attributes['invoice_number'] ?? null; - } - - public function getInvoiceDateAttribute(): ?string - { - return $this->attributes['invoice_date'] ?? null; - } - - public function getInvoiceAmountAttribute(): ?float - { - return isset($this->attributes['invoice_amount']) ? (float) $this->attributes['invoice_amount'] : null; - } - - public function vendor(): BelongsTo - { - return $this->belongsTo(Vendor::class); - } - - public function warehouse(): BelongsTo - { - return $this->belongsTo(Warehouse::class); - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public function items(): HasMany - { - return $this->hasMany(PurchaseOrderItem::class); - } - - public function getActivitylogOptions(): LogOptions - { - return LogOptions::defaults() - ->logAll() - ->logOnlyDirty() - ->dontSubmitEmptyLogs(); - } - - public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) - { - $properties = $activity->properties; - $attributes = $properties['attributes'] ?? []; - $snapshot = $properties['snapshot'] ?? []; - - // Snapshot key names - $snapshot['po_number'] = $this->code; - $snapshot['vendor_name'] = $this->vendor ? $this->vendor->name : null; - $snapshot['warehouse_name'] = $this->warehouse ? $this->warehouse->name : null; - $snapshot['user_name'] = $this->user ? $this->user->name : null; - - $properties['attributes'] = $attributes; - $properties['snapshot'] = $snapshot; - $activity->properties = $properties; - } -} diff --git a/app/Models/PurchaseOrderItem.php b/app/Models/PurchaseOrderItem.php deleted file mode 100644 index 4e8e731..0000000 --- a/app/Models/PurchaseOrderItem.php +++ /dev/null @@ -1,68 +0,0 @@ - 'decimal:2', - 'unit_price' => 'decimal:2', - 'subtotal' => 'decimal:2', - 'received_quantity' => 'decimal:2', - ]; - - public function getProductNameAttribute(): string - { - return $this->product?->name ?? ''; - } - - // 關聯單位 - public function unit(): \Illuminate\Database\Eloquent\Relations\BelongsTo - { - return $this->belongsTo(Unit::class); - } - - public function getUnitNameAttribute(): string - { - // 優先使用關聯的 unit - if ($this->unit) { - return $this->unit->name; - } - - if (!$this->product) { - return ''; - } - - // Fallback: 嘗試從 Product 的關聯單位獲取 - return $this->product->purchaseUnit?->name - ?? $this->product->largeUnit?->name - ?? $this->product->baseUnit?->name - ?? ''; - } - - public function purchaseOrder(): BelongsTo - { - return $this->belongsTo(PurchaseOrder::class); - } - - public function product(): BelongsTo - { - return $this->belongsTo(Product::class); - } -} diff --git a/app/Models/UtilityFee.php b/app/Models/UtilityFee.php deleted file mode 100644 index aadaefc..0000000 --- a/app/Models/UtilityFee.php +++ /dev/null @@ -1,24 +0,0 @@ - 'date:Y-m-d', - 'amount' => 'decimal:2', - ]; -} diff --git a/app/Modules/.gitkeep b/app/Modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Http/Controllers/Admin/ActivityLogController.php b/app/Modules/Core/Controllers/ActivityLogController.php similarity index 82% rename from app/Http/Controllers/Admin/ActivityLogController.php rename to app/Modules/Core/Controllers/ActivityLogController.php index 0e351e0..23bd36b 100644 --- a/app/Http/Controllers/Admin/ActivityLogController.php +++ b/app/Modules/Core/Controllers/ActivityLogController.php @@ -1,8 +1,9 @@ '使用者', - 'App\Models\Role' => '角色', - 'App\Models\Product' => '商品', - 'App\Models\Vendor' => '廠商', - 'App\Models\Category' => '商品分類', - 'App\Models\Unit' => '單位', - 'App\Models\PurchaseOrder' => '採購單', - 'App\Models\Warehouse' => '倉庫', - 'App\Models\Inventory' => '庫存', - 'App\Models\UtilityFee' => '公共事業費', + 'App\Modules\Core\Models\User' => '使用者', + 'App\Modules\Core\Models\Role' => '角色', + 'App\Modules\Inventory\Models\Product' => '商品', + 'App\Modules\Procurement\Models\Vendor' => '廠商', + 'App\Modules\Inventory\Models\Category' => '商品分類', + 'App\Modules\Inventory\Models\Unit' => '單位', + 'App\Modules\Procurement\Models\PurchaseOrder' => '採購單', + 'App\Modules\Inventory\Models\Warehouse' => '倉庫', + 'App\Modules\Inventory\Models\Inventory' => '庫存', + 'App\Modules\Finance\Models\UtilityFee' => '公共事業費', ]; } @@ -101,7 +102,7 @@ class ActivityLogController extends Controller })->values(); // Get users for causer filter - $users = \App\Models\User::select('id', 'name')->orderBy('name')->get() + $users = \App\Modules\Core\Models\User::select('id', 'name')->orderBy('name')->get() ->map(function ($user) { return ['label' => $user->name, 'value' => (string) $user->id]; }); diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Modules/Core/Controllers/Auth/LoginController.php similarity index 97% rename from app/Http/Controllers/Auth/LoginController.php rename to app/Modules/Core/Controllers/Auth/LoginController.php index a8d64f3..661fb1b 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Modules/Core/Controllers/Auth/LoginController.php @@ -1,6 +1,6 @@ name('login'); +Route::post('/login', [LoginController::class, 'store']); +Route::post('/logout', [LoginController::class, 'destroy'])->name('logout'); + +Route::middleware('auth')->group(function () { + // 儀表板 - 所有登入使用者皆可存取 + Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); + + // 使用者帳號設定 + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password'); + + // 系統管理 + Route::prefix('admin')->group(function () { + Route::middleware('permission:roles.view')->group(function () { + Route::get('/roles', [RoleController::class, 'index'])->name('roles.index'); + Route::middleware('permission:roles.create')->group(function () { + Route::get('/roles/create', [RoleController::class, 'create'])->name('roles.create'); + Route::post('/roles', [RoleController::class, 'store'])->name('roles.store'); + }); + Route::get('/roles/{role}/edit', [RoleController::class, 'edit'])->middleware('permission:roles.edit')->name('roles.edit'); + Route::put('/roles/{role}', [RoleController::class, 'update'])->middleware('permission:roles.edit')->name('roles.update'); + Route::delete('/roles/{role}', [RoleController::class, 'destroy'])->middleware('permission:roles.delete')->name('roles.destroy'); + }); + + Route::middleware('permission:users.view')->group(function () { + Route::get('/users', [UserController::class, 'index'])->name('users.index'); + Route::middleware('permission:users.create')->group(function () { + Route::get('/users/create', [UserController::class, 'create'])->name('users.create'); + Route::post('/users', [UserController::class, 'store'])->name('users.store'); + }); + Route::get('/users/{user}/edit', [UserController::class, 'edit'])->middleware('permission:users.edit')->name('users.edit'); + Route::put('/users/{user}', [UserController::class, 'update'])->middleware('permission:users.edit')->name('users.update'); + Route::delete('/users/{user}', [UserController::class, 'destroy'])->middleware('permission:users.delete')->name('users.destroy'); + }); + + Route::middleware('permission:system.view_logs')->group(function () { + Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('activity-logs.index'); + }); + + }); +}); diff --git a/app/Http/Controllers/AccountingReportController.php b/app/Modules/Finance/Controllers/AccountingReportController.php similarity index 97% rename from app/Http/Controllers/AccountingReportController.php rename to app/Modules/Finance/Controllers/AccountingReportController.php index c65dfe6..9ceb9e7 100644 --- a/app/Http/Controllers/AccountingReportController.php +++ b/app/Modules/Finance/Controllers/AccountingReportController.php @@ -1,9 +1,11 @@ */ + use HasFactory; + + protected $fillable = [ + 'type', // 'electricity', 'water', 'gas', etc. + 'billing_period_start', + 'billing_period_end', + 'due_date', + 'amount', + 'usage_amount', // kWh, m3, etc. + 'unit', // 度, 立方米 + 'status', // 'pending', 'paid', 'overdue' + 'paid_at', + 'payment_method', + 'notes', + 'receipt_image_path', + ]; + + protected $casts = [ + 'billing_period_start' => 'date', + 'billing_period_end' => 'date', + 'due_date' => 'date', + 'paid_at' => 'datetime', + 'amount' => 'decimal:2', + 'usage_amount' => 'decimal:2', + ]; +} diff --git a/app/Modules/Finance/Routes/web.php b/app/Modules/Finance/Routes/web.php new file mode 100644 index 0000000..9cb8cfe --- /dev/null +++ b/app/Modules/Finance/Routes/web.php @@ -0,0 +1,29 @@ +group(function () { + // 公共事業費管理 + Route::middleware('permission:utility_fees.view')->group(function () { + Route::get('/utility-fees', [UtilityFeeController::class, 'index'])->name('utility-fees.index'); + }); + Route::middleware('permission:utility_fees.create')->group(function () { + Route::post('/utility-fees', [UtilityFeeController::class, 'store'])->name('utility-fees.store'); + }); + Route::middleware('permission:utility_fees.edit')->group(function () { + Route::put('/utility-fees/{utility_fee}', [UtilityFeeController::class, 'update'])->name('utility-fees.update'); + }); + Route::middleware('permission:utility_fees.delete')->group(function () { + Route::delete('/utility-fees/{utility_fee}', [UtilityFeeController::class, 'destroy'])->name('utility-fees.destroy'); + }); + + // 會計報表 + Route::middleware('permission:accounting.view')->prefix('accounting-report')->group(function () { + Route::get('/', [AccountingReportController::class, 'index'])->name('accounting.report'); + Route::get('/export', [AccountingReportController::class, 'export']) + ->middleware('permission:accounting.export') + ->name('accounting.export'); + }); +}); diff --git a/app/Http/Controllers/CategoryController.php b/app/Modules/Inventory/Controllers/CategoryController.php similarity index 91% rename from app/Http/Controllers/CategoryController.php rename to app/Modules/Inventory/Controllers/CategoryController.php index ee4dd3e..8eebcee 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Modules/Inventory/Controllers/CategoryController.php @@ -1,8 +1,10 @@ load([ 'inventories.product.category', @@ -15,7 +17,7 @@ class InventoryController extends Controller 'inventories.lastIncomingTransaction', 'inventories.lastOutgoingTransaction' ]); - $allProducts = \App\Models\Product::with('category')->get(); + $allProducts = \App\Modules\Inventory\Models\Product::with('category')->get(); // 1. 準備 availableProducts $availableProducts = $allProducts->map(function ($product) { @@ -104,10 +106,10 @@ class InventoryController extends Controller ]); } - public function create(\App\Models\Warehouse $warehouse) + public function create(\App\Modules\Inventory\Models\Warehouse $warehouse) { // 取得所有商品供前端選單使用 - $products = \App\Models\Product::with(['baseUnit', 'largeUnit']) + $products = \App\Modules\Inventory\Models\Product::with(['baseUnit', 'largeUnit']) ->select('id', 'name', 'code', 'base_unit_id', 'large_unit_id', 'conversion_rate') ->get() ->map(function ($product) { @@ -127,7 +129,7 @@ class InventoryController extends Controller ]); } - public function store(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse) + public function store(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse) { $validated = $request->validate([ 'inboundDate' => 'required|date', @@ -148,16 +150,16 @@ class InventoryController extends Controller if ($item['batchMode'] === 'existing') { // 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加) - $inventory = \App\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']); + $inventory = \App\Modules\Inventory\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']); if ($inventory->trashed()) { $inventory->restore(); } } else { // 模式 B:建立新批號 $originCountry = $item['originCountry'] ?? 'TW'; - $product = \App\Models\Product::find($item['productId']); + $product = \App\Modules\Inventory\Models\Product::find($item['productId']); - $batchNumber = \App\Models\Inventory::generateBatchNumber( + $batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber( $product->code ?? 'UNK', $originCountry, $validated['inboundDate'] @@ -208,12 +210,12 @@ class InventoryController extends Controller /** * API: 取得商品在特定倉庫的所有批號,並回傳當前日期/產地下的一個流水號 */ - public function getBatches(\App\Models\Warehouse $warehouse, $productId, \Illuminate\Http\Request $request) + public function getBatches(\App\Modules\Inventory\Models\Warehouse $warehouse, $productId, \Illuminate\Http\Request $request) { $originCountry = $request->query('originCountry', 'TW'); $arrivalDate = $request->query('arrivalDate', now()->format('Y-m-d')); - $batches = \App\Models\Inventory::where('warehouse_id', $warehouse->id) + $batches = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $warehouse->id) ->where('product_id', $productId) ->get() ->map(function ($inventory) { @@ -227,10 +229,10 @@ class InventoryController extends Controller }); // 計算下一個流水號 - $product = \App\Models\Product::find($productId); + $product = \App\Modules\Inventory\Models\Product::find($productId); $nextSequence = '01'; if ($product) { - $batchNumber = \App\Models\Inventory::generateBatchNumber( + $batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber( $product->code ?? 'UNK', $originCountry, $arrivalDate @@ -244,7 +246,7 @@ class InventoryController extends Controller ]); } - public function edit(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId) + public function edit(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId) { // 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人) // 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理 @@ -252,7 +254,7 @@ class InventoryController extends Controller return redirect()->back()->with('error', '無法編輯範例資料'); } - $inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) { + $inventory = \App\Modules\Inventory\Models\Inventory::with(['product', 'transactions' => function($query) { $query->orderBy('actual_time', 'desc')->orderBy('id', 'desc'); }, 'transactions.user'])->findOrFail($inventoryId); @@ -289,13 +291,13 @@ class InventoryController extends Controller ]); } - public function update(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId) + public function update(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId) { // 若是 product ID (舊邏輯),先轉為 inventory // 但新路由我們傳的是 inventory ID // 為了相容,我們先判斷 $inventoryId 是 inventory ID - $inventory = \App\Models\Inventory::find($inventoryId); + $inventory = \App\Modules\Inventory\Models\Inventory::find($inventoryId); // 如果找不到 (可能是舊路由傳 product ID) if (!$inventory) { @@ -393,9 +395,9 @@ class InventoryController extends Controller }); } - public function destroy(\App\Models\Warehouse $warehouse, $inventoryId) + public function destroy(\App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId) { - $inventory = \App\Models\Inventory::findOrFail($inventoryId); + $inventory = \App\Modules\Inventory\Models\Inventory::findOrFail($inventoryId); // 庫存 > 0 不允許刪除 (哪怕是軟刪除) if ($inventory->quantity > 0) { @@ -421,14 +423,14 @@ class InventoryController extends Controller ->with('success', '庫存品項已刪除'); } - public function history(Request $request, \App\Models\Warehouse $warehouse) + public function history(Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse) { $inventoryId = $request->query('inventoryId'); $productId = $request->query('productId'); if ($productId) { // 商品層級查詢 - $inventories = \App\Models\Inventory::where('warehouse_id', $warehouse->id) + $inventories = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $warehouse->id) ->where('product_id', $productId) ->with(['product', 'transactions' => function($query) { $query->orderBy('actual_time', 'desc')->orderBy('id', 'desc'); @@ -503,7 +505,7 @@ class InventoryController extends Controller if ($inventoryId) { // 單一批號查詢 - $inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) { + $inventory = \App\Modules\Inventory\Models\Inventory::with(['product', 'transactions' => function($query) { $query->orderBy('actual_time', 'desc')->orderBy('id', 'desc'); }, 'transactions.user'])->findOrFail($inventoryId); diff --git a/app/Http/Controllers/ProductController.php b/app/Modules/Inventory/Controllers/ProductController.php similarity index 95% rename from app/Http/Controllers/ProductController.php rename to app/Modules/Inventory/Controllers/ProductController.php index 6207b4e..f8348fc 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Modules/Inventory/Controllers/ProductController.php @@ -1,9 +1,11 @@ paginate($perPage)->withQueryString(); - $categories = \App\Models\Category::where('is_active', true)->get(); + $categories = \App\Modules\Inventory\Models\Category::where('is_active', true)->get(); return Inertia::render('Product/Index', [ 'products' => $products, diff --git a/app/Http/Controllers/SafetyStockController.php b/app/Modules/Inventory/Controllers/SafetyStockController.php similarity index 93% rename from app/Http/Controllers/SafetyStockController.php rename to app/Modules/Inventory/Controllers/SafetyStockController.php index b0449a7..69c0168 100644 --- a/app/Http/Controllers/SafetyStockController.php +++ b/app/Modules/Inventory/Controllers/SafetyStockController.php @@ -1,11 +1,13 @@ 'boolean', - ]; - - /** - * Get the products for the category. - */ public function products(): HasMany { return $this->hasMany(Product::class); } - public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions + public function getActivitylogOptions(): LogOptions { - return \Spatie\Activitylog\LogOptions::defaults() + return LogOptions::defaults() ->logAll() ->logOnlyDirty() ->dontSubmitEmptyLogs(); diff --git a/app/Models/Inventory.php b/app/Modules/Inventory/Models/Inventory.php similarity index 97% rename from app/Models/Inventory.php rename to app/Modules/Inventory/Models/Inventory.php index 4868459..7dcb64b 100644 --- a/app/Models/Inventory.php +++ b/app/Modules/Inventory/Models/Inventory.php @@ -1,9 +1,10 @@ */ use HasFactory, LogsActivity; - protected $fillable = [ - 'name', - 'code', - ]; + protected $fillable = ['name', 'abbreviation']; - public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions + public function productsAsBase(): HasMany { - return \Spatie\Activitylog\LogOptions::defaults() + return $this->hasMany(Product::class, 'base_unit_id'); + } + + public function productsAsLarge(): HasMany + { + return $this->hasMany(Product::class, 'large_unit_id'); + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() ->logAll() ->logOnlyDirty() ->dontSubmitEmptyLogs(); diff --git a/app/Models/Warehouse.php b/app/Modules/Inventory/Models/Warehouse.php similarity index 92% rename from app/Models/Warehouse.php rename to app/Modules/Inventory/Models/Warehouse.php index aa7d64e..2e332e0 100644 --- a/app/Models/Warehouse.php +++ b/app/Modules/Inventory/Models/Warehouse.php @@ -1,9 +1,10 @@ group(function () { + + // 類別管理 (用於商品對話框) - 需要商品權限 + Route::middleware('permission:products.view')->group(function () { + Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); + Route::post('/categories', [CategoryController::class, 'store'])->middleware('permission:products.create')->name('categories.store'); + Route::put('/categories/{category}', [CategoryController::class, 'update'])->middleware('permission:products.edit')->name('categories.update'); + Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->middleware('permission:products.delete')->name('categories.destroy'); + }); + + // 單位管理 - 需要商品權限 + Route::middleware('permission:products.create|products.edit')->group(function () { + Route::post('/units', [UnitController::class, 'store'])->name('units.store'); + Route::put('/units/{unit}', [UnitController::class, 'update'])->name('units.update'); + Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->name('units.destroy'); + }); + + // 商品管理 + Route::middleware('permission:products.view')->group(function () { + Route::get('/products', [ProductController::class, 'index'])->name('products.index'); + Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store'); + Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update'); + Route::delete('/products/{product}', [ProductController::class, 'destroy'])->middleware('permission:products.delete')->name('products.destroy'); + }); + + // 倉庫管理 + Route::middleware('permission:warehouses.view')->group(function () { + Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index'); + Route::post('/warehouses', [WarehouseController::class, 'store'])->middleware('permission:warehouses.create')->name('warehouses.store'); + Route::put('/warehouses/{warehouse}', [WarehouseController::class, 'update'])->middleware('permission:warehouses.edit')->name('warehouses.update'); + Route::delete('/warehouses/{warehouse}', [WarehouseController::class, 'destroy'])->middleware('permission:warehouses.delete')->name('warehouses.destroy'); + + // 倉庫庫存管理 - 需要庫存權限 + Route::middleware('permission:inventory.view')->group(function () { + Route::get('/warehouses/{warehouse}/inventory', [InventoryController::class, 'index'])->name('warehouses.inventory.index'); + Route::get('/warehouses/{warehouse}/inventory-history', [InventoryController::class, 'history'])->name('warehouses.inventory.history'); + + Route::middleware('permission:inventory.adjust')->group(function () { + Route::get('/warehouses/{warehouse}/inventory/create', [InventoryController::class, 'create'])->name('warehouses.inventory.create'); + Route::post('/warehouses/{warehouse}/inventory', [InventoryController::class, 'store'])->name('warehouses.inventory.store'); + Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/edit', [InventoryController::class, 'edit'])->name('warehouses.inventory.edit'); + Route::put('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'update'])->name('warehouses.inventory.update'); + Route::delete('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy'); + }); + + // API: 取得商品在特定倉庫的所有批號 + Route::get('/api/warehouses/{warehouse}/inventory/batches/{productId}', [InventoryController::class, 'getBatches']) + ->name('api.warehouses.inventory.batches'); + }); + + // 安全庫存設定 + Route::middleware('permission:inventory.view')->group(function () { + Route::get('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'index'])->name('warehouses.safety-stock.index'); + Route::middleware('permission:inventory.safety_stock')->group(function () { + Route::post('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'store'])->name('warehouses.safety-stock.store'); + Route::put('/warehouses/{warehouse}/safety-stock/{safetyStock}', [SafetyStockController::class, 'update'])->name('warehouses.safety-stock.update'); + Route::delete('/warehouses/{warehouse}/safety-stock/{safetyStock}', [SafetyStockController::class, 'destroy'])->name('warehouses.safety-stock.destroy'); + }); + }); + }); + + // 撥補單 (在庫存調撥時使用) + Route::middleware('permission:inventory.transfer')->group(function () { + Route::post('/transfer-orders', [TransferOrderController::class, 'store'])->name('transfer-orders.store'); + }); + Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories']) + ->middleware('permission:inventory.view') + ->name('api.warehouses.inventories'); +}); diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Modules/Procurement/Controllers/PurchaseOrderController.php similarity index 98% rename from app/Http/Controllers/PurchaseOrderController.php rename to app/Modules/Procurement/Controllers/PurchaseOrderController.php index 6ff462b..c16fced 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Modules/Procurement/Controllers/PurchaseOrderController.php @@ -1,10 +1,12 @@ id(); if (!$userId) { - $user = \App\Models\User::first(); + $user = \App\Modules\Core\Models\User::first(); if (!$user) { - $user = \App\Models\User::create([ + $user = \App\Modules\Core\Models\User::create([ 'name' => '系統管理員', 'email' => 'admin@example.com', 'password' => bcrypt('password'), diff --git a/app/Http/Controllers/VendorController.php b/app/Modules/Procurement/Controllers/VendorController.php similarity index 94% rename from app/Http/Controllers/VendorController.php rename to app/Modules/Procurement/Controllers/VendorController.php index 6919205..8992c62 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Modules/Procurement/Controllers/VendorController.php @@ -1,8 +1,10 @@ load(['products.baseUnit', 'products.largeUnit']); return \Inertia\Inertia::render('Vendor/Show', [ 'vendor' => $vendor, - 'products' => \App\Models\Product::with('baseUnit')->get(), + 'products' => \App\Modules\Inventory\Models\Product::with('baseUnit')->get(), ]); } diff --git a/app/Http/Controllers/VendorProductController.php b/app/Modules/Procurement/Controllers/VendorProductController.php similarity index 91% rename from app/Http/Controllers/VendorProductController.php rename to app/Modules/Procurement/Controllers/VendorProductController.php index 6a25f8f..4b4fad9 100644 --- a/app/Http/Controllers/VendorProductController.php +++ b/app/Modules/Procurement/Controllers/VendorProductController.php @@ -1,8 +1,10 @@ performedOn($vendor) ->withProperties([ @@ -66,7 +68,7 @@ class VendorProductController extends Controller ]); // 記錄操作 - $product = \App\Models\Product::find($productId); + $product = \App\Modules\Inventory\Models\Product::find($productId); activity() ->performedOn($vendor) ->withProperties([ @@ -95,7 +97,7 @@ class VendorProductController extends Controller public function destroy(Vendor $vendor, $productId) { // 記錄操作 (需在 detach 前獲取資訊) - $product = \App\Models\Product::find($productId); + $product = \App\Modules\Inventory\Models\Product::find($productId); $old_price = $vendor->products()->where('product_id', $productId)->first()?->pivot?->last_price; $vendor->products()->detach($productId); diff --git a/app/Modules/Procurement/Models/PurchaseOrder.php b/app/Modules/Procurement/Models/PurchaseOrder.php new file mode 100644 index 0000000..0161889 --- /dev/null +++ b/app/Modules/Procurement/Models/PurchaseOrder.php @@ -0,0 +1,79 @@ + */ + use HasFactory; + use \Spatie\Activitylog\Traits\LogsActivity; + + protected $fillable = [ + 'po_number', + 'vendor_id', + 'warehouse_id', + 'user_id', + 'order_date', + 'expected_delivery_date', + 'status', + 'total_amount', + 'notes', + ]; + + protected $casts = [ + 'order_date' => 'date', + 'expected_delivery_date' => 'date', + 'total_amount' => 'decimal:2', + ]; + + public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions + { + return \Spatie\Activitylog\LogOptions::defaults() + ->logAll() + ->logOnlyDirty() + ->dontSubmitEmptyLogs(); + } + + public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) + { + $snapshot = $activity->properties['snapshot'] ?? []; + + $snapshot['po_number'] = $this->po_number; + + if ($this->vendor) { + $snapshot['vendor_name'] = $this->vendor->name; + } + if ($this->warehouse) { + $snapshot['warehouse_name'] = $this->warehouse->name; + } + + $activity->properties = $activity->properties->merge([ + 'snapshot' => $snapshot + ]); + } + + public function vendor(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Vendor::class); + } + + public function warehouse(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Warehouse::class); + } + + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class); + } + + public function items(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(PurchaseOrderItem::class); + } +} diff --git a/app/Modules/Procurement/Models/PurchaseOrderItem.php b/app/Modules/Procurement/Models/PurchaseOrderItem.php new file mode 100644 index 0000000..de53384 --- /dev/null +++ b/app/Modules/Procurement/Models/PurchaseOrderItem.php @@ -0,0 +1,44 @@ + */ + use HasFactory; + + protected $fillable = [ + 'purchase_order_id', + 'product_id', + 'quantity', + 'unit_price', + 'subtotal', + // 驗收欄位 + 'received_quantity', + // 批號與效期 (驗收時填寫) + 'batch_number', + 'expiry_date', + ]; + + protected $casts = [ + 'quantity' => 'decimal:2', + 'unit_price' => 'decimal:4', + 'subtotal' => 'decimal:2', + 'received_quantity' => 'decimal:2', + 'expiry_date' => 'date', + ]; + + public function purchaseOrder(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(PurchaseOrder::class); + } + + public function product(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Product::class); + } +} diff --git a/app/Models/Vendor.php b/app/Modules/Procurement/Models/Vendor.php similarity index 53% rename from app/Models/Vendor.php rename to app/Modules/Procurement/Models/Vendor.php index 85e4f12..144c29a 100644 --- a/app/Models/Vendor.php +++ b/app/Modules/Procurement/Models/Vendor.php @@ -1,38 +1,35 @@ */ + use HasFactory, LogsActivity; protected $fillable = [ 'code', 'name', - 'short_name', - 'tax_id', - 'owner', - 'contact_name', - 'tel', - 'phone', + 'contact_person', 'email', + 'phone', 'address', - 'remark' + 'tax_id', + 'payment_terms', ]; - public function products(): BelongsToMany + + public function products(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { - return $this->belongsToMany(Product::class, 'product_vendor') - ->withPivot('last_price') - ->withTimestamps(); + return $this->belongsToMany(Product::class)->withPivot('last_price')->withTimestamps(); } - public function purchaseOrders(): HasMany + public function purchaseOrders(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(PurchaseOrder::class); } @@ -49,12 +46,8 @@ class Vendor extends Model { $properties = $activity->properties; - // Store name in 'snapshot' for context, keeping 'attributes' clean $snapshot = $properties['snapshot'] ?? []; - // Only set name if it's not already set (e.g. by controller for specific context like supply product) - if (!isset($snapshot['name'])) { - $snapshot['name'] = $this->name; - } + $snapshot['name'] = $this->name; $properties['snapshot'] = $snapshot; $activity->properties = $properties; diff --git a/app/Modules/Procurement/Routes/web.php b/app/Modules/Procurement/Routes/web.php new file mode 100644 index 0000000..5275186 --- /dev/null +++ b/app/Modules/Procurement/Routes/web.php @@ -0,0 +1,38 @@ +group(function () { + // 廠商管理 + Route::middleware('permission:vendors.view')->group(function () { + Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); + Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show'); + Route::post('/vendors', [VendorController::class, 'store'])->middleware('permission:vendors.create')->name('vendors.store'); + Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.update'); + Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->middleware('permission:vendors.delete')->name('vendors.destroy'); + + // 供貨商品相關路由 + Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->middleware('permission:vendors.edit')->name('vendors.products.store'); + Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.products.update'); + Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->middleware('permission:vendors.edit')->name('vendors.products.destroy'); + }); + + // 採購單管理 + Route::middleware('permission:purchase_orders.view')->group(function () { + Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index'); + + Route::middleware('permission:purchase_orders.create')->group(function () { + Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); + Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); + }); + + Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show'); + + Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.edit'); + Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.update'); + Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->middleware('permission:purchase_orders.delete')->name('purchase-orders.destroy'); + }); +}); diff --git a/app/Http/Controllers/ProductionOrderController.php b/app/Modules/Production/Controllers/ProductionOrderController.php similarity index 98% rename from app/Http/Controllers/ProductionOrderController.php rename to app/Modules/Production/Controllers/ProductionOrderController.php index 887dac4..c312e9e 100644 --- a/app/Http/Controllers/ProductionOrderController.php +++ b/app/Modules/Production/Controllers/ProductionOrderController.php @@ -1,13 +1,14 @@ */ + use HasFactory; + + protected $fillable = [ + 'code', + 'product_id', + 'warehouse_id', + 'output_quantity', + 'output_batch_number', + 'output_box_count', + 'production_date', + 'expiry_date', + 'user_id', + 'status', + 'remark', + ]; + + public static function generateCode() + { + $prefix = 'PO' . now()->format('Ymd'); + $lastOrder = self::where('code', 'like', $prefix . '%')->latest()->first(); + if ($lastOrder) { + $lastSequence = intval(substr($lastOrder->code, -3)); + $sequence = str_pad($lastSequence + 1, 3, '0', STR_PAD_LEFT); + } else { + $sequence = '001'; + } + return $prefix . $sequence; + } + + protected $casts = [ + 'order_date' => 'date', + 'start_date' => 'datetime', + 'completion_date' => 'datetime', + 'quantity' => 'decimal:2', + 'produced_quantity' => 'decimal:2', + ]; + + public function product(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Product::class); + } + + public function warehouse(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Warehouse::class); + } + + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class); + } + + public function items(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ProductionOrderItem::class); + } +} diff --git a/app/Modules/Production/Models/ProductionOrderItem.php b/app/Modules/Production/Models/ProductionOrderItem.php new file mode 100644 index 0000000..23bc855 --- /dev/null +++ b/app/Modules/Production/Models/ProductionOrderItem.php @@ -0,0 +1,44 @@ + */ + use HasFactory; + + protected $fillable = [ + 'production_order_id', + 'inventory_id', + 'quantity_used', + 'unit_id', + ]; + + protected $casts = [ + 'quantity_used' => 'decimal:4', + ]; + + public function inventory() + { + return $this->belongsTo(\App\Modules\Inventory\Models\Inventory::class); + } + + public function unit() + { + return $this->belongsTo(\App\Modules\Inventory\Models\Unit::class); + } + + public function productionOrder(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(ProductionOrder::class); + } + + public function product(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Product::class); + } +} diff --git a/app/Modules/Production/Routes/web.php b/app/Modules/Production/Routes/web.php new file mode 100644 index 0000000..b4e8a66 --- /dev/null +++ b/app/Modules/Production/Routes/web.php @@ -0,0 +1,28 @@ +group(function () { + // 生產管理 + Route::middleware('permission:production_orders.view')->group(function () { + Route::get('/production-orders', [ProductionOrderController::class, 'index'])->name('production-orders.index'); + + Route::middleware('permission:production_orders.create')->group(function () { + Route::get('/production-orders/create', [ProductionOrderController::class, 'create'])->name('production-orders.create'); + Route::post('/production-orders', [ProductionOrderController::class, 'store'])->name('production-orders.store'); + }); + + Route::get('/production-orders/{productionOrder}', [ProductionOrderController::class, 'show'])->name('production-orders.show'); + + Route::middleware('permission:production_orders.edit')->group(function () { + Route::get('/production-orders/{productionOrder}/edit', [ProductionOrderController::class, 'edit'])->name('production-orders.edit'); + Route::put('/production-orders/{productionOrder}', [ProductionOrderController::class, 'update'])->name('production-orders.update'); + }); + }); + + // 生產管理 API + Route::get('/api/production/warehouses/{warehouse}/inventories', [ProductionOrderController::class, 'getWarehouseInventories']) + ->middleware('permission:production_orders.create') + ->name('api.production.warehouses.inventories'); +}); diff --git a/app/Providers/ModuleServiceProvider.php b/app/Providers/ModuleServiceProvider.php new file mode 100644 index 0000000..6e81662 --- /dev/null +++ b/app/Providers/ModuleServiceProvider.php @@ -0,0 +1,40 @@ +group($routesPath); + } + } + } + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index c6a4f12..7e01db1 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,4 +3,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\TenancyServiceProvider::class, + App\Providers\ModuleServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 78f8669..460cb70 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "autoload": { "psr-4": { "App\\": "app/", + "App\\Modules\\": "app/Modules/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" } @@ -92,4 +93,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..3e9d705 100644 --- a/config/auth.php +++ b/config/auth.php @@ -62,7 +62,7 @@ return [ 'providers' => [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\User::class), + 'model' => env('AUTH_MODEL', App\Modules\Core\Models\User::class), ], // 'users' => [ diff --git a/config/permission.php b/config/permission.php index 03bf909..5710828 100644 --- a/config/permission.php +++ b/config/permission.php @@ -24,7 +24,7 @@ return [ * `Spatie\Permission\Contracts\Role` contract. */ - 'role' => App\Models\Role::class, + 'role' => App\Modules\Core\Models\Role::class, ], diff --git a/config/tenancy.php b/config/tenancy.php index 6ce878c..1dc715d 100644 --- a/config/tenancy.php +++ b/config/tenancy.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Stancl\Tenancy\Database\Models\Domain; -use App\Models\Tenant; +use App\Modules\Core\Models\Tenant; return [ 'tenant_model' => Tenant::class, diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php index 1f620c0..04a2ad6 100644 --- a/database/factories/ProductFactory.php +++ b/database/factories/ProductFactory.php @@ -2,11 +2,11 @@ namespace Database\Factories; -use App\Models\Category; +use App\Modules\Inventory\Models\Category; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product> + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Modules\Inventory\Models\Product> */ class ProductFactory extends Factory { diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..a32daed 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Modules\Core\Models\User> */ class UserFactory extends Factory { diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php index 9714d65..6234323 100644 --- a/database/seeders/CategorySeeder.php +++ b/database/seeders/CategorySeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Models\Category; +use App\Modules\Inventory\Models\Category; use Illuminate\Database\Seeder; class CategorySeeder extends Seeder diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 1468bcc..56e17a8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Models\User; +use App\Modules\Core\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 9bf8fdf..bf46f4a 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -5,7 +5,7 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; -use App\Models\User; +use App\Modules\Core\Models\User; class PermissionSeeder extends Seeder { diff --git a/database/seeders/ProductSeeder.php b/database/seeders/ProductSeeder.php index ba86999..5c92a49 100644 --- a/database/seeders/ProductSeeder.php +++ b/database/seeders/ProductSeeder.php @@ -2,8 +2,8 @@ namespace Database\Seeders; -use App\Models\Category; -use App\Models\Product; +use App\Modules\Inventory\Models\Category; +use App\Modules\Inventory\Models\Product; use Illuminate\Database\Seeder; class ProductSeeder extends Seeder diff --git a/database/seeders/TenantDatabaseSeeder.php b/database/seeders/TenantDatabaseSeeder.php index 7a86914..dfc3cd5 100644 --- a/database/seeders/TenantDatabaseSeeder.php +++ b/database/seeders/TenantDatabaseSeeder.php @@ -3,7 +3,7 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; -use App\Models\User; +use App\Modules\Core\Models\User; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; diff --git a/database/seeders/UnitSeeder.php b/database/seeders/UnitSeeder.php index 07eb690..d914994 100644 --- a/database/seeders/UnitSeeder.php +++ b/database/seeders/UnitSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Models\Unit; +use App\Modules\Inventory\Models\Unit; use Illuminate\Database\Seeder; class UnitSeeder extends Seeder diff --git a/database/seeders/VendorSeeder.php b/database/seeders/VendorSeeder.php index 95fc146..7fc8260 100644 --- a/database/seeders/VendorSeeder.php +++ b/database/seeders/VendorSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Models\Vendor; +use App\Modules\Procurement\Models\Vendor; use Illuminate\Database\Seeder; class VendorSeeder extends Seeder diff --git a/docs/FRAMEWORK_SPEC.md b/docs/FRAMEWORK_SPEC.md new file mode 100644 index 0000000..53c0ce9 --- /dev/null +++ b/docs/FRAMEWORK_SPEC.md @@ -0,0 +1,60 @@ +# 開發框架規範說明書:ERP 系統 (star-erp) + +## 1. 專案概述 +* **目標**: 打造一個強大且穩定的 ERP 後台管理系統。 +* **核心架構**: 採用 **模組化單體式架構 (Modular Monolith)** 配現代化前端。使用 Laravel、Inertia.js 及 React。 +* **工作流程**: 將 UI/UX 設計師提供的 React 原始碼,透過 Inertia.js 整合進 Laravel 環境中。後端邏輯依據「業務領域」拆分為獨立模組。 + +## 2. 技術棧 (Tech Stack) +* **後端**: PHP 8.5 / Laravel 12 +* **前端橋樑**: Inertia.js (不使用傳統 RESTful API 串接頁面,改用 Inertia 協議) +* **前端庫**: React (以 Functional Components 與 Hooks 為主) +* **樣式處理**: Tailwind CSS (確保與 UI/UX 設計稿完全一致) +* **資料庫**: MySQL 8.0 +* **開發環境**: Laravel Sail (Docker / WSL2) +* **未來擴充**: 針對高併發或跨平台模組,預留 Golang 微服務接口。 + +## 3. 目錄結構與慣例 + +### 3.1 後端 (Laravel - Modular Monolith) +系統採用模組化架構,核心邏輯位於 `app/Modules/` 下: + +* **Modules**: 位於 `app/Modules/{ModuleName}/`。 + * **Controllers**: `app/Modules/{ModuleName}/Controllers/`。必須回傳 `Inertia::render()`。 + * **Models**: `app/Modules/{ModuleName}/Models/`。 + * **Routes**: `app/Modules/{ModuleName}/Routes/web.php`。各模組獨立管理路由。 +* **Global Routes**: `routes/web.php` 僅保留全域通用路由或作為模組路由的載入點。 + +### 3.2 前端 (React) +* **Pages (頁面)**: 位於 `resources/js/Pages/`。每個檔案代表一個完整的路由視圖。 +* **Components (組件)**: 位於 `resources/js/Components/`。存放由 UI/UX 團隊提供的可重複使用 UI 元件。 +* **Layouts (版面)**: 位於 `resources/js/Layouts/`。定義 ERP 的通用版面。 + +## 4. 整合指南 (UI/UX 轉換至 Laravel) +* **組件遷移**: 將 UI/UX 的 React 原始碼移入 `resources/js/` 時,應進行「原子化」拆解,提高元件複用率。 +* **資料傳遞**: 透過 Laravel Controller 的 props 傳送動態資料給 React。優先使用 Inertia 資料流,避免初次渲染時使用 axios。 +* **狀態管理**: 優先使用 Inertia 內建的狀態管理與 useForm hook 處理表單提交。 + +## 5. 開發標準 (Coding Standards) +* **命名規範**: + * Controllers: `PascalCaseController.php` + * React Components: `PascalCase.jsx` + * Routes: `kebab-case` (小寫橫線分隔) +* **回傳格式**: 所有的前後端溝通需維持一致的 JSON 結構,特別是驗證錯誤 (Validation Errors) 與閃存訊息 (Flash Messages)。 + +## 6. AI 協作規則 (給 Antigravity AI) +* **角色設定**: 你是一位專業的全端開發工程師助手。 +* **代碼生成指令**: + * 所有的解釋說明請使用 **繁體中文**。 + * 生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。 + * 必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。 + * 新增功能時,請先判斷應歸屬於哪個 Module,並建立在 `app/Modules/` 對應目錄下。 + +## 7. 運行機制 (Docker / Sail) +由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令: + +* **啟動環境**: `./vendor/bin/sail up -d` +* **執行 PHP 指令**: `./vendor/bin/sail php -v` +* **執行 Artisan 指令**: `./vendor/bin/sail artisan route:list` +* **執行 Composer**: `./vendor/bin/sail composer install` +* **執行 Node/NPM**: `./vendor/bin/sail npm run dev` \ No newline at end of file diff --git a/docs/MODULAR_ARCHITECTURE.md b/docs/MODULAR_ARCHITECTURE.md new file mode 100644 index 0000000..481fe3d --- /dev/null +++ b/docs/MODULAR_ARCHITECTURE.md @@ -0,0 +1,92 @@ +# Star ERP 模組化單體架構 (Modular Monolith) + +本文件記錄 Star ERP 的模組化架構現狀、模組邊界定義以及各模組包含之詳細功能。 + +## 1. 架構概觀 +系統採用 **模組化單體 (Modular Monolith)** 架構。 +- **後端**:依據業務領域 (Domain) 拆分為獨立模組,位於 `app/Modules/{ModuleName}`。 +- **前端**:維持統一的 Inertia/React 架構,位於 `resources/js`。 +- **通訊**:模組間優先透過 Service Class 溝通,但允許在同一資料庫內進行關聯查詢 (Eloquent Relationships)。 + +--- + +## 2. 模組列表與功能 (Modules Manifest) + +### ✅ Inventory (庫存模組) +**定位**:處理所有與「商品」及「實體庫存」相關的業務。通用於所有產業。 +- **Namespace**: `App\Modules\Inventory` +- **狀態**: 🟢 已遷移 (Migrated) +- **功能細項**: + - **商品基礎資料**: + - 商品管理 (CRUD、多規格) + - 商品分類 (Category) + - 計量單位 (Unit, 支援大小單位換算) + - **倉庫管理**: + - 多倉庫設定 (Warehouse) + - 庫存查詢 (Inventory Lookup) + - 庫存異動歷史 (Transaction History) + - **庫存作業**: + - 手動庫存調整 (Adjustments) + - 庫存調撥 (Transfer Orders) + - 批號追蹤 (Batch Tracking, 基礎版) + - **監控**: + - 安全庫存設定 (Safety Stock) + +--- + +### ✅ Core (系統核心模組) +**定位**:系統基礎設施,處理帳號、權限與租戶管理。 +- **Namespace**: `App\Modules\Core` +- **狀態**: 🟢 已遷移 (Migrated) +- **功能細項**: + - **身分驗證**: 登入/登出 (Auth) + - **使用者管理**: User CRUD + - **權限控制**: 角色與權限 (RBAC) + - **多租戶**: 租戶管理 (Tenancy) + - **系統監控**: 操作紀錄 (Activity Log) + - **個人化**: 個人設定 (Profile) + +--- + +### ✅ Procurement (採購模組) +**定位**:供應鏈管理,處理進貨源頭。 +- **Namespace**: `App\Modules\Procurement` +- **狀態**: 🟢 已遷移 (Migrated) +- **功能細項**: + - **供應商管理**: 廠商資料 (Vendor)、供貨商品清單 + - **採購作業**: 採購單 (Purchase Order)、進貨驗收 + +--- + +### ✅ Production (生產模組) +**定位**:製造與加工,食品業/製造業核心。 +- **Namespace**: `App\Modules\Production` +- **狀態**: 🟢 已遷移 (Migrated) +- **功能細項**: + - **工單管理**: 生產工單 (Production Order) + - **配方管理**: (規劃中) Recipe + - **領料與耗用**: 原料扣庫 + +--- + +### ✅ Finance (財務模組) +**定位**:經營分析與帳務。 +- **Namespace**: `App\Modules\Finance` +- **狀態**: 🟢 已遷移 (Migrated) +- **功能細項**: + - **費用管理**: 公共事業費 (Utility Fee) + - **報表**: 會計報表 (Accounting Reports) + - **成本分析**: (規劃中) Costing + +--- + +## 3. 未來擴充模組 (Future Verticals) + +針對特定產業的垂直擴充模組(可插拔): + +| 模組名稱 | 適用產業 | 關鍵功能 | +| :--- | :--- | :--- | +| **Logistics** | 物流/零售 | 路徑規劃、裝車單、司機派送 | +| **Food** | 食品/餐飲 | 嚴格效期控管 (FEFO)、雙向溯源、營養成分標示 | +| **Retail** | 零售/電商 | 全通路訂單整合、促銷引擎 (Promotion)、POS 介接 | +| **Cosmetics**| 化妝品 | 成分分析、過敏原管理 | diff --git a/resources/js/utils/format.ts b/resources/js/utils/format.ts index 498f298..77da518 100644 --- a/resources/js/utils/format.ts +++ b/resources/js/utils/format.ts @@ -5,14 +5,16 @@ /** * 格式化數字為千分位格式 */ -export const formatNumber = (num: number): string => { +export const formatNumber = (num: number | null | undefined): string => { + if (num === null || num === undefined) return "0"; return num.toLocaleString(); }; /** * 格式化貨幣(NT$) */ -export const formatCurrency = (num: number): string => { +export const formatCurrency = (num: number | null | undefined): string => { + if (num === null || num === undefined) return "NT$ 0"; return `NT$ ${num.toLocaleString()}`; }; diff --git a/routes/web.php b/routes/web.php index ce73284..a1cbbdc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,211 +2,37 @@ use Illuminate\Support\Facades\Route; use Inertia\Inertia; -use App\Http\Controllers\CategoryController; -use App\Http\Controllers\VendorController; -use App\Http\Controllers\VendorProductController; -use App\Http\Controllers\DashboardController; -use App\Http\Controllers\ProductController; -use App\Http\Controllers\Auth\LoginController; -use App\Http\Controllers\PurchaseOrderController; -use App\Http\Controllers\WarehouseController; -use App\Http\Controllers\InventoryController; -use App\Http\Controllers\SafetyStockController; -use App\Http\Controllers\TransferOrderController; -use App\Http\Controllers\UnitController; -use App\Http\Controllers\Admin\RoleController; -use App\Http\Controllers\Admin\UserController; -use App\Http\Controllers\Admin\ActivityLogController; -use App\Http\Controllers\ProfileController; -use App\Http\Controllers\UtilityFeeController; -use App\Http\Controllers\AccountingReportController; -use App\Http\Controllers\ProductionOrderController; + + + + + + use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; // 登入/登出路由 -Route::get('/login', [LoginController::class, 'show'])->name('login'); -Route::post('/login', [LoginController::class, 'store']); -Route::post('/logout', [LoginController::class, 'destroy'])->name('logout'); + Route::middleware('auth')->group(function () { // 儀表板 - 所有登入使用者皆可存取 - Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); - // 使用者帳號設定 - Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); - Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); - Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password'); - // 類別管理 (用於商品對話框) - 需要商品權限 - Route::middleware('permission:products.view')->group(function () { - Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); - Route::post('/categories', [CategoryController::class, 'store'])->middleware('permission:products.create')->name('categories.store'); - Route::put('/categories/{category}', [CategoryController::class, 'update'])->middleware('permission:products.edit')->name('categories.update'); - Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->middleware('permission:products.delete')->name('categories.destroy'); - }); - // 單位管理 - 需要商品權限 - Route::middleware('permission:products.create|products.edit')->group(function () { - Route::post('/units', [UnitController::class, 'store'])->name('units.store'); - Route::put('/units/{unit}', [UnitController::class, 'update'])->name('units.update'); - Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->name('units.destroy'); - }); - // 商品管理 - Route::middleware('permission:products.view')->group(function () { - Route::get('/products', [ProductController::class, 'index'])->name('products.index'); - Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store'); - Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update'); - Route::delete('/products/{product}', [ProductController::class, 'destroy'])->middleware('permission:products.delete')->name('products.destroy'); - }); - // 廠商管理 - Route::middleware('permission:vendors.view')->group(function () { - Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); - Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show'); - Route::post('/vendors', [VendorController::class, 'store'])->middleware('permission:vendors.create')->name('vendors.store'); - Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.update'); - Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->middleware('permission:vendors.delete')->name('vendors.destroy'); - // 供貨商品相關路由 - Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->middleware('permission:vendors.edit')->name('vendors.products.store'); - Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.products.update'); - Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->middleware('permission:vendors.edit')->name('vendors.products.destroy'); - }); - // 倉庫管理 - Route::middleware('permission:warehouses.view')->group(function () { - Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index'); - Route::post('/warehouses', [WarehouseController::class, 'store'])->middleware('permission:warehouses.create')->name('warehouses.store'); - Route::put('/warehouses/{warehouse}', [WarehouseController::class, 'update'])->middleware('permission:warehouses.edit')->name('warehouses.update'); - Route::delete('/warehouses/{warehouse}', [WarehouseController::class, 'destroy'])->middleware('permission:warehouses.delete')->name('warehouses.destroy'); - // 倉庫庫存管理 - 需要庫存權限 - Route::middleware('permission:inventory.view')->group(function () { - Route::get('/warehouses/{warehouse}/inventory', [InventoryController::class, 'index'])->name('warehouses.inventory.index'); - Route::get('/warehouses/{warehouse}/inventory-history', [InventoryController::class, 'history'])->name('warehouses.inventory.history'); - - Route::middleware('permission:inventory.adjust')->group(function () { - Route::get('/warehouses/{warehouse}/inventory/create', [InventoryController::class, 'create'])->name('warehouses.inventory.create'); - Route::post('/warehouses/{warehouse}/inventory', [InventoryController::class, 'store'])->name('warehouses.inventory.store'); - Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/edit', [InventoryController::class, 'edit'])->name('warehouses.inventory.edit'); - Route::put('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'update'])->name('warehouses.inventory.update'); - Route::delete('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy'); - }); - // API: 取得商品在特定倉庫的所有批號 - Route::get('/api/warehouses/{warehouse}/inventory/batches/{productId}', [InventoryController::class, 'getBatches']) - ->name('api.warehouses.inventory.batches'); - }); - // 安全庫存設定 - Route::middleware('permission:inventory.view')->group(function () { - Route::get('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'index'])->name('warehouses.safety-stock.index'); - Route::middleware('permission:inventory.safety_stock')->group(function () { - Route::post('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'store'])->name('warehouses.safety-stock.store'); - Route::put('/warehouses/{warehouse}/safety-stock/{safetyStock}', [SafetyStockController::class, 'update'])->name('warehouses.safety-stock.update'); - Route::delete('/warehouses/{warehouse}/safety-stock/{safetyStock}', [SafetyStockController::class, 'destroy'])->name('warehouses.safety-stock.destroy'); - }); - }); - }); - // 採購單管理 - Route::middleware('permission:purchase_orders.view')->group(function () { - Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index'); - - Route::middleware('permission:purchase_orders.create')->group(function () { - Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); - Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); - }); - Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show'); - - Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.edit'); - Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.update'); - Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->middleware('permission:purchase_orders.delete')->name('purchase-orders.destroy'); - }); - // 公共事業費管理 (TODO: 添加權限控制) - // 公共事業費 - Route::middleware('permission:utility_fees.view')->group(function () { - Route::get('/utility-fees', [UtilityFeeController::class, 'index'])->name('utility-fees.index'); - }); - Route::middleware('permission:utility_fees.create')->group(function () { - Route::post('/utility-fees', [UtilityFeeController::class, 'store'])->name('utility-fees.store'); - }); - Route::middleware('permission:utility_fees.edit')->group(function () { - Route::put('/utility-fees/{utility_fee}', [UtilityFeeController::class, 'update'])->name('utility-fees.update'); - }); - Route::middleware('permission:utility_fees.delete')->group(function () { - Route::delete('/utility-fees/{utility_fee}', [UtilityFeeController::class, 'destroy'])->name('utility-fees.destroy'); - }); - // 撥補單 (在庫存調撥時使用) - Route::middleware('permission:inventory.transfer')->group(function () { - Route::post('/transfer-orders', [TransferOrderController::class, 'store'])->name('transfer-orders.store'); - }); - Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories']) - ->middleware('permission:inventory.view') - ->name('api.warehouses.inventories'); - // 系統管理 - Route::middleware('permission:accounting.view')->prefix('accounting-report')->group(function () { - Route::get('/', [AccountingReportController::class, 'index'])->name('accounting.report'); - Route::get('/export', [AccountingReportController::class, 'export']) - ->middleware('permission:accounting.export') - ->name('accounting.export'); - }); - // 生產管理 - Route::middleware('permission:production_orders.view')->group(function () { - Route::get('/production-orders', [ProductionOrderController::class, 'index'])->name('production-orders.index'); - - Route::middleware('permission:production_orders.create')->group(function () { - Route::get('/production-orders/create', [ProductionOrderController::class, 'create'])->name('production-orders.create'); - Route::post('/production-orders', [ProductionOrderController::class, 'store'])->name('production-orders.store'); - }); - - Route::get('/production-orders/{productionOrder}', [ProductionOrderController::class, 'show'])->name('production-orders.show'); - - Route::middleware('permission:production_orders.edit')->group(function () { - Route::get('/production-orders/{productionOrder}/edit', [ProductionOrderController::class, 'edit'])->name('production-orders.edit'); - Route::put('/production-orders/{productionOrder}', [ProductionOrderController::class, 'update'])->name('production-orders.update'); - }); - }); - // 生產管理 API - Route::get('/api/production/warehouses/{warehouse}/inventories', [ProductionOrderController::class, 'getWarehouseInventories']) - ->middleware('permission:production_orders.create') - ->name('api.production.warehouses.inventories'); - // 系統管理 - Route::prefix('admin')->group(function () { - Route::middleware('permission:roles.view')->group(function () { - Route::get('/roles', [RoleController::class, 'index'])->name('roles.index'); - Route::middleware('permission:roles.create')->group(function () { - Route::get('/roles/create', [RoleController::class, 'create'])->name('roles.create'); - Route::post('/roles', [RoleController::class, 'store'])->name('roles.store'); - }); - Route::get('/roles/{role}/edit', [RoleController::class, 'edit'])->middleware('permission:roles.edit')->name('roles.edit'); - Route::put('/roles/{role}', [RoleController::class, 'update'])->middleware('permission:roles.edit')->name('roles.update'); - Route::delete('/roles/{role}', [RoleController::class, 'destroy'])->middleware('permission:roles.delete')->name('roles.destroy'); - }); - - Route::middleware('permission:users.view')->group(function () { - Route::get('/users', [UserController::class, 'index'])->name('users.index'); - Route::middleware('permission:users.create')->group(function () { - Route::get('/users/create', [UserController::class, 'create'])->name('users.create'); - Route::post('/users', [UserController::class, 'store'])->name('users.store'); - }); - Route::get('/users/{user}/edit', [UserController::class, 'edit'])->middleware('permission:users.edit')->name('users.edit'); - Route::put('/users/{user}', [UserController::class, 'update'])->middleware('permission:users.edit')->name('users.update'); - Route::delete('/users/{user}', [UserController::class, 'destroy'])->middleware('permission:users.delete')->name('users.destroy'); - }); - Route::middleware('permission:system.view_logs')->group(function () { - Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('activity-logs.index'); - }); - - }); }); // End of auth middleware group