chore: 完善模組化架構遷移與修復前端顯示錯誤
- 修正所有模組 Controller 的 Model 引用路徑 (App\Modules\...) - 更新 ProductionOrder 與 ProductionOrderItem 模型結構以符合新版邏輯 - 修復 resources/js/utils/format.ts 在處理空值時導致 toLocaleString 崩潰的問題 - 清除全域路徑與 Controller 遷移殘留檔案
This commit is contained in:
@@ -5,73 +5,64 @@ trigger: always_on
|
|||||||
---
|
---
|
||||||
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)
|
## 2. 技術棧 (Tech Stack)
|
||||||
後端: PHP 8.5 / Laravel 12
|
* **後端**: 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. 目錄結構與慣例
|
## 6. AI 協作規則 (給 Antigravity AI)
|
||||||
3.1 後端 (Laravel)
|
* **角色設定**: 你是一位專業的全端開發工程師助手。
|
||||||
Controllers: 必須回傳 Inertia::render() 來渲染頁面。
|
* **代碼生成指令**:
|
||||||
|
* 所有的解釋說明請使用 **繁體中文**。
|
||||||
|
* 生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。
|
||||||
|
* 必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。
|
||||||
|
* 新增功能時,請先判斷應歸屬於哪個 Module,並建立在 `app/Modules/` 對應目錄下。
|
||||||
|
|
||||||
Models: 嚴格執行型別標註,使用 Eloquent 進行資料庫操作。
|
## 7. 運行機制 (Docker / Sail)
|
||||||
|
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
|
||||||
|
|
||||||
Routes: 統一在 routes/web.php 定義 Inertia 路由。
|
* **啟動環境**: `./vendor/bin/sail up -d`
|
||||||
|
* **執行 PHP 指令**: `./vendor/bin/sail php -v`
|
||||||
3.2 前端 (React)
|
* **執行 Artisan 指令**: `./vendor/bin/sail artisan route:list`
|
||||||
Pages (頁面): 位於 resources/js/Pages/。每個檔案代表一個完整的路由視圖。
|
* **執行 Composer**: `./vendor/bin/sail composer install`
|
||||||
|
* **執行 Node/NPM**: `./vendor/bin/sail npm run dev`
|
||||||
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
|
|
||||||
@@ -101,8 +101,8 @@ public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, st
|
|||||||
protected function getSubjectMap()
|
protected function getSubjectMap()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'App\Models\Product' => '商品',
|
'App\Modules\Inventory\Models\Product' => '商品',
|
||||||
'App\Models\UtilityFee' => '公共事業費', // ✅ 新增映射
|
'App\Modules\Finance\Models\UtilityFee' => '公共事業費', // ✅ 新增映射
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Tenant;
|
use App\Modules\Core\Models\Tenant;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Landlord;
|
namespace App\Http\Controllers\Landlord;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Tenant;
|
use App\Modules\Core\Models\Tenant;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Landlord;
|
namespace App\Http\Controllers\Landlord;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Tenant;
|
use App\Modules\Core\Models\Tenant;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
|
||||||
use Spatie\Activitylog\LogOptions;
|
|
||||||
|
|
||||||
class ProductionOrder extends Model
|
|
||||||
{
|
|
||||||
use HasFactory, LogsActivity;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'code',
|
|
||||||
'product_id',
|
|
||||||
'output_batch_number',
|
|
||||||
'output_box_count',
|
|
||||||
'output_quantity',
|
|
||||||
'warehouse_id',
|
|
||||||
'production_date',
|
|
||||||
'expiry_date',
|
|
||||||
'user_id',
|
|
||||||
'status',
|
|
||||||
'remark',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'production_date' => '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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class ProductionOrderItem extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'production_order_id',
|
|
||||||
'inventory_id',
|
|
||||||
'quantity_used',
|
|
||||||
'unit_id',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'quantity_used' => '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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
|
||||||
use Spatie\Activitylog\LogOptions;
|
|
||||||
|
|
||||||
class PurchaseOrder extends Model
|
|
||||||
{
|
|
||||||
use HasFactory, LogsActivity;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'code',
|
|
||||||
'vendor_id',
|
|
||||||
'warehouse_id',
|
|
||||||
'user_id',
|
|
||||||
'status',
|
|
||||||
'expected_delivery_date',
|
|
||||||
'total_amount',
|
|
||||||
'tax_amount',
|
|
||||||
'grand_total',
|
|
||||||
'remark',
|
|
||||||
'invoice_number',
|
|
||||||
'invoice_date',
|
|
||||||
'invoice_amount',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'expected_delivery_date' => '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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class PurchaseOrderItem extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'purchase_order_id',
|
|
||||||
'product_id',
|
|
||||||
'quantity',
|
|
||||||
'unit_id', // 新增單位ID欄位
|
|
||||||
'unit_price',
|
|
||||||
'subtotal',
|
|
||||||
'received_quantity',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'quantity' => '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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class UtilityFee extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'transaction_date',
|
|
||||||
'category',
|
|
||||||
'amount',
|
|
||||||
'invoice_number',
|
|
||||||
'description',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'transaction_date' => 'date:Y-m-d',
|
|
||||||
'amount' => 'decimal:2',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
0
app/Modules/.gitkeep
Normal file
0
app/Modules/.gitkeep
Normal file
@@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Modules\Core\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Spatie\Activitylog\Models\Activity;
|
use Spatie\Activitylog\Models\Activity;
|
||||||
@@ -12,16 +13,16 @@ class ActivityLogController extends Controller
|
|||||||
private function getSubjectMap()
|
private function getSubjectMap()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'App\Models\User' => '使用者',
|
'App\Modules\Core\Models\User' => '使用者',
|
||||||
'App\Models\Role' => '角色',
|
'App\Modules\Core\Models\Role' => '角色',
|
||||||
'App\Models\Product' => '商品',
|
'App\Modules\Inventory\Models\Product' => '商品',
|
||||||
'App\Models\Vendor' => '廠商',
|
'App\Modules\Procurement\Models\Vendor' => '廠商',
|
||||||
'App\Models\Category' => '商品分類',
|
'App\Modules\Inventory\Models\Category' => '商品分類',
|
||||||
'App\Models\Unit' => '單位',
|
'App\Modules\Inventory\Models\Unit' => '單位',
|
||||||
'App\Models\PurchaseOrder' => '採購單',
|
'App\Modules\Procurement\Models\PurchaseOrder' => '採購單',
|
||||||
'App\Models\Warehouse' => '倉庫',
|
'App\Modules\Inventory\Models\Warehouse' => '倉庫',
|
||||||
'App\Models\Inventory' => '庫存',
|
'App\Modules\Inventory\Models\Inventory' => '庫存',
|
||||||
'App\Models\UtilityFee' => '公共事業費',
|
'App\Modules\Finance\Models\UtilityFee' => '公共事業費',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ class ActivityLogController extends Controller
|
|||||||
})->values();
|
})->values();
|
||||||
|
|
||||||
// Get users for causer filter
|
// 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) {
|
->map(function ($user) {
|
||||||
return ['label' => $user->name, 'value' => (string) $user->id];
|
return ['label' => $user->name, 'value' => (string) $user->id];
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Modules\Core\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Core\Controllers;
|
||||||
|
|
||||||
use App\Models\Product;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Vendor;
|
|
||||||
use App\Models\PurchaseOrder;
|
use App\Modules\Inventory\Models\Product;
|
||||||
use App\Models\Warehouse;
|
use App\Modules\Procurement\Models\Vendor;
|
||||||
use App\Models\Inventory;
|
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||||
use App\Models\WarehouseProductSafetyStock;
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
|
use App\Modules\Inventory\Models\Inventory;
|
||||||
|
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Core\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules\Password;
|
use Illuminate\Validation\Rules\Password;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Modules\Core\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Modules\Core\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
|
||||||
|
use App\Modules\Core\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Core\Models;
|
||||||
|
|
||||||
use Spatie\Permission\Models\Role as SpatieRole;
|
use Spatie\Permission\Models\Role as SpatieRole;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Core\Models;
|
||||||
|
|
||||||
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
|
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
|
||||||
use Stancl\Tenancy\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Contracts\TenantWithDatabase;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Core\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
54
app/Modules/Core/Routes/web.php
Normal file
54
app/Modules/Core/Routes/web.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Modules\Core\Controllers\Auth\LoginController;
|
||||||
|
use App\Modules\Core\Controllers\DashboardController;
|
||||||
|
use App\Modules\Core\Controllers\ProfileController;
|
||||||
|
use App\Modules\Core\Controllers\RoleController;
|
||||||
|
use App\Modules\Core\Controllers\UserController;
|
||||||
|
use App\Modules\Core\Controllers\ActivityLogController;
|
||||||
|
|
||||||
|
// 登入/登出路由
|
||||||
|
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::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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Finance\Controllers;
|
||||||
|
|
||||||
use App\Models\PurchaseOrder;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\UtilityFee;
|
|
||||||
|
use App\Modules\Finance\Models\UtilityFee;
|
||||||
|
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Finance\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Modules\Finance\Models\UtilityFee;
|
||||||
|
|
||||||
use App\Models\UtilityFee;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
36
app/Modules/Finance/Models/UtilityFee.php
Normal file
36
app/Modules/Finance/Models/UtilityFee.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Finance\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UtilityFee extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UtilityFeeFactory> */
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
29
app/Modules/Finance/Routes/web.php
Normal file
29
app/Modules/Finance/Routes/web.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Modules\Finance\Controllers\UtilityFeeController;
|
||||||
|
use App\Modules\Finance\Controllers\AccountingReportController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
use App\Models\Category;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
use App\Modules\Inventory\Models\Category;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class CategoryController extends Controller
|
class CategoryController extends Controller
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\WarehouseProductSafetyStock;
|
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||||
|
|
||||||
class InventoryController extends Controller
|
class InventoryController extends Controller
|
||||||
{
|
{
|
||||||
public function index(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse)
|
public function index(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse)
|
||||||
{
|
{
|
||||||
$warehouse->load([
|
$warehouse->load([
|
||||||
'inventories.product.category',
|
'inventories.product.category',
|
||||||
@@ -15,7 +17,7 @@ class InventoryController extends Controller
|
|||||||
'inventories.lastIncomingTransaction',
|
'inventories.lastIncomingTransaction',
|
||||||
'inventories.lastOutgoingTransaction'
|
'inventories.lastOutgoingTransaction'
|
||||||
]);
|
]);
|
||||||
$allProducts = \App\Models\Product::with('category')->get();
|
$allProducts = \App\Modules\Inventory\Models\Product::with('category')->get();
|
||||||
|
|
||||||
// 1. 準備 availableProducts
|
// 1. 準備 availableProducts
|
||||||
$availableProducts = $allProducts->map(function ($product) {
|
$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')
|
->select('id', 'name', 'code', 'base_unit_id', 'large_unit_id', 'conversion_rate')
|
||||||
->get()
|
->get()
|
||||||
->map(function ($product) {
|
->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([
|
$validated = $request->validate([
|
||||||
'inboundDate' => 'required|date',
|
'inboundDate' => 'required|date',
|
||||||
@@ -148,16 +150,16 @@ class InventoryController extends Controller
|
|||||||
|
|
||||||
if ($item['batchMode'] === 'existing') {
|
if ($item['batchMode'] === 'existing') {
|
||||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||||
$inventory = \App\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
$inventory = \App\Modules\Inventory\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||||
if ($inventory->trashed()) {
|
if ($inventory->trashed()) {
|
||||||
$inventory->restore();
|
$inventory->restore();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 模式 B:建立新批號
|
// 模式 B:建立新批號
|
||||||
$originCountry = $item['originCountry'] ?? 'TW';
|
$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',
|
$product->code ?? 'UNK',
|
||||||
$originCountry,
|
$originCountry,
|
||||||
$validated['inboundDate']
|
$validated['inboundDate']
|
||||||
@@ -208,12 +210,12 @@ class InventoryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* API: 取得商品在特定倉庫的所有批號,並回傳當前日期/產地下的一個流水號
|
* 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');
|
$originCountry = $request->query('originCountry', 'TW');
|
||||||
$arrivalDate = $request->query('arrivalDate', now()->format('Y-m-d'));
|
$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)
|
->where('product_id', $productId)
|
||||||
->get()
|
->get()
|
||||||
->map(function ($inventory) {
|
->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';
|
$nextSequence = '01';
|
||||||
if ($product) {
|
if ($product) {
|
||||||
$batchNumber = \App\Models\Inventory::generateBatchNumber(
|
$batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber(
|
||||||
$product->code ?? 'UNK',
|
$product->code ?? 'UNK',
|
||||||
$originCountry,
|
$originCountry,
|
||||||
$arrivalDate
|
$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),需要特殊處理
|
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
|
||||||
@@ -252,7 +254,7 @@ class InventoryController extends Controller
|
|||||||
return redirect()->back()->with('error', '無法編輯範例資料');
|
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');
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
}, '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
|
// 若是 product ID (舊邏輯),先轉為 inventory
|
||||||
// 但新路由我們傳的是 inventory ID
|
// 但新路由我們傳的是 inventory ID
|
||||||
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
|
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
|
||||||
|
|
||||||
$inventory = \App\Models\Inventory::find($inventoryId);
|
$inventory = \App\Modules\Inventory\Models\Inventory::find($inventoryId);
|
||||||
|
|
||||||
// 如果找不到 (可能是舊路由傳 product ID)
|
// 如果找不到 (可能是舊路由傳 product ID)
|
||||||
if (!$inventory) {
|
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 不允許刪除 (哪怕是軟刪除)
|
// 庫存 > 0 不允許刪除 (哪怕是軟刪除)
|
||||||
if ($inventory->quantity > 0) {
|
if ($inventory->quantity > 0) {
|
||||||
@@ -421,14 +423,14 @@ class InventoryController extends Controller
|
|||||||
->with('success', '庫存品項已刪除');
|
->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');
|
$inventoryId = $request->query('inventoryId');
|
||||||
$productId = $request->query('productId');
|
$productId = $request->query('productId');
|
||||||
|
|
||||||
if ($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)
|
->where('product_id', $productId)
|
||||||
->with(['product', 'transactions' => function($query) {
|
->with(['product', 'transactions' => function($query) {
|
||||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||||
@@ -503,7 +505,7 @@ class InventoryController extends Controller
|
|||||||
|
|
||||||
if ($inventoryId) {
|
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');
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||||
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
use App\Models\Product;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Unit;
|
|
||||||
|
use App\Modules\Inventory\Models\Product;
|
||||||
|
use App\Modules\Inventory\Models\Unit;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@@ -59,7 +61,7 @@ class ProductController extends Controller
|
|||||||
|
|
||||||
$products = $query->paginate($perPage)->withQueryString();
|
$products = $query->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', [
|
return Inertia::render('Product/Index', [
|
||||||
'products' => $products,
|
'products' => $products,
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
use App\Models\Warehouse;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\WarehouseProductSafetyStock;
|
|
||||||
use App\Models\Product;
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
use App\Models\Inventory;
|
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||||
|
use App\Modules\Inventory\Models\Product;
|
||||||
|
use App\Modules\Inventory\Models\Inventory;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
use App\Models\Inventory;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Warehouse;
|
|
||||||
|
use App\Modules\Inventory\Models\Inventory;
|
||||||
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
use App\Models\Unit;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Product; // Import Product to check for usage
|
|
||||||
|
use App\Modules\Inventory\Models\Unit;
|
||||||
|
use App\Modules\Inventory\Models\Product; // Import Product to check for usage
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class UnitController extends Controller
|
class UnitController extends Controller
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Inventory\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
use App\Models\Warehouse;
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
@@ -1,37 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
|
||||||
class Category extends Model
|
class Category extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, LogsActivity;
|
use HasFactory, LogsActivity;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = ['name', 'description'];
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'is_active',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the products for the category.
|
|
||||||
*/
|
|
||||||
public function products(): HasMany
|
public function products(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Product::class);
|
return $this->hasMany(Product::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
public function getActivitylogOptions(): LogOptions
|
||||||
{
|
{
|
||||||
return \Spatie\Activitylog\LogOptions::defaults()
|
return LogOptions::defaults()
|
||||||
->logAll()
|
->logAll()
|
||||||
->logOnlyDirty()
|
->logOnlyDirty()
|
||||||
->dontSubmitEmptyLogs();
|
->dontSubmitEmptyLogs();
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Modules\Procurement\Models\PurchaseOrder; // Cross-module dependency
|
||||||
|
|
||||||
class Inventory extends Model
|
class Inventory extends Model
|
||||||
{
|
{
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Models\Inventory;
|
use App\Modules\Core\Models\User; // Cross-module Core dependency
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
class InventoryTransaction extends Model
|
class InventoryTransaction extends Model
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
use App\Modules\Procurement\Models\Vendor; // Cross-module dependency (Procurement)
|
||||||
|
|
||||||
class Product extends Model
|
class Product extends Model
|
||||||
{
|
{
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
|
||||||
class Unit extends Model
|
class Unit extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UnitFactory> */
|
|
||||||
use HasFactory, LogsActivity;
|
use HasFactory, LogsActivity;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = ['name', 'abbreviation'];
|
||||||
'name',
|
|
||||||
'code',
|
|
||||||
];
|
|
||||||
|
|
||||||
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()
|
->logAll()
|
||||||
->logOnlyDirty()
|
->logOnlyDirty()
|
||||||
->dontSubmitEmptyLogs();
|
->dontSubmitEmptyLogs();
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Modules\Procurement\Models\PurchaseOrder; // Cross-module dependency (Procurement)
|
||||||
|
|
||||||
class Warehouse extends Model
|
class Warehouse extends Model
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Inventory\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
80
app/Modules/Inventory/Routes/web.php
Normal file
80
app/Modules/Inventory/Routes/web.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Modules\Inventory\Controllers\CategoryController;
|
||||||
|
use App\Modules\Inventory\Controllers\UnitController;
|
||||||
|
use App\Modules\Inventory\Controllers\ProductController;
|
||||||
|
use App\Modules\Inventory\Controllers\WarehouseController;
|
||||||
|
use App\Modules\Inventory\Controllers\InventoryController;
|
||||||
|
use App\Modules\Inventory\Controllers\SafetyStockController;
|
||||||
|
use App\Modules\Inventory\Controllers\TransferOrderController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->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');
|
||||||
|
});
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Procurement\Controllers;
|
||||||
|
|
||||||
use App\Models\PurchaseOrder;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Vendor;
|
|
||||||
use App\Models\Warehouse;
|
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||||
|
use App\Modules\Procurement\Models\Vendor;
|
||||||
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -146,9 +148,9 @@ class PurchaseOrderController extends Controller
|
|||||||
// 確保有一個有效的使用者 ID
|
// 確保有一個有效的使用者 ID
|
||||||
$userId = auth()->id();
|
$userId = auth()->id();
|
||||||
if (!$userId) {
|
if (!$userId) {
|
||||||
$user = \App\Models\User::first();
|
$user = \App\Modules\Core\Models\User::first();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
$user = \App\Models\User::create([
|
$user = \App\Modules\Core\Models\User::create([
|
||||||
'name' => '系統管理員',
|
'name' => '系統管理員',
|
||||||
'email' => 'admin@example.com',
|
'email' => 'admin@example.com',
|
||||||
'password' => bcrypt('password'),
|
'password' => bcrypt('password'),
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Procurement\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Modules\Procurement\Models\Vendor;
|
||||||
|
|
||||||
use App\Models\Vendor;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class VendorController extends Controller
|
class VendorController extends Controller
|
||||||
@@ -56,7 +58,7 @@ class VendorController extends Controller
|
|||||||
$vendor->load(['products.baseUnit', 'products.largeUnit']);
|
$vendor->load(['products.baseUnit', 'products.largeUnit']);
|
||||||
return \Inertia\Inertia::render('Vendor/Show', [
|
return \Inertia\Inertia::render('Vendor/Show', [
|
||||||
'vendor' => $vendor,
|
'vendor' => $vendor,
|
||||||
'products' => \App\Models\Product::with('baseUnit')->get(),
|
'products' => \App\Modules\Inventory\Models\Product::with('baseUnit')->get(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Procurement\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Modules\Procurement\Models\Vendor;
|
||||||
|
|
||||||
use App\Models\Vendor;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ class VendorProductController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 記錄操作
|
// 記錄操作
|
||||||
$product = \App\Models\Product::find($validated['product_id']);
|
$product = \App\Modules\Inventory\Models\Product::find($validated['product_id']);
|
||||||
activity()
|
activity()
|
||||||
->performedOn($vendor)
|
->performedOn($vendor)
|
||||||
->withProperties([
|
->withProperties([
|
||||||
@@ -66,7 +68,7 @@ class VendorProductController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 記錄操作
|
// 記錄操作
|
||||||
$product = \App\Models\Product::find($productId);
|
$product = \App\Modules\Inventory\Models\Product::find($productId);
|
||||||
activity()
|
activity()
|
||||||
->performedOn($vendor)
|
->performedOn($vendor)
|
||||||
->withProperties([
|
->withProperties([
|
||||||
@@ -95,7 +97,7 @@ class VendorProductController extends Controller
|
|||||||
public function destroy(Vendor $vendor, $productId)
|
public function destroy(Vendor $vendor, $productId)
|
||||||
{
|
{
|
||||||
// 記錄操作 (需在 detach 前獲取資訊)
|
// 記錄操作 (需在 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;
|
$old_price = $vendor->products()->where('product_id', $productId)->first()?->pivot?->last_price;
|
||||||
|
|
||||||
$vendor->products()->detach($productId);
|
$vendor->products()->detach($productId);
|
||||||
79
app/Modules/Procurement/Models/PurchaseOrder.php
Normal file
79
app/Modules/Procurement/Models/PurchaseOrder.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Procurement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
|
use App\Modules\Core\Models\User;
|
||||||
|
|
||||||
|
class PurchaseOrder extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\PurchaseOrderFactory> */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Modules/Procurement/Models/PurchaseOrderItem.php
Normal file
44
app/Modules/Procurement/Models/PurchaseOrderItem.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Procurement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Modules\Inventory\Models\Product;
|
||||||
|
|
||||||
|
class PurchaseOrderItem extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\PurchaseOrderItemFactory> */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Modules\Procurement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
use App\Modules\Inventory\Models\Product;
|
||||||
|
|
||||||
class Vendor extends Model
|
class Vendor extends Model
|
||||||
{
|
{
|
||||||
use LogsActivity;
|
/** @use HasFactory<\Database\Factories\VendorFactory> */
|
||||||
|
use HasFactory, LogsActivity;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'code',
|
'code',
|
||||||
'name',
|
'name',
|
||||||
'short_name',
|
'contact_person',
|
||||||
'tax_id',
|
|
||||||
'owner',
|
|
||||||
'contact_name',
|
|
||||||
'tel',
|
|
||||||
'phone',
|
|
||||||
'email',
|
'email',
|
||||||
|
'phone',
|
||||||
'address',
|
'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')
|
return $this->belongsToMany(Product::class)->withPivot('last_price')->withTimestamps();
|
||||||
->withPivot('last_price')
|
|
||||||
->withTimestamps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function purchaseOrders(): HasMany
|
public function purchaseOrders(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(PurchaseOrder::class);
|
return $this->hasMany(PurchaseOrder::class);
|
||||||
}
|
}
|
||||||
@@ -49,12 +46,8 @@ class Vendor extends Model
|
|||||||
{
|
{
|
||||||
$properties = $activity->properties;
|
$properties = $activity->properties;
|
||||||
|
|
||||||
// Store name in 'snapshot' for context, keeping 'attributes' clean
|
|
||||||
$snapshot = $properties['snapshot'] ?? [];
|
$snapshot = $properties['snapshot'] ?? [];
|
||||||
// Only set name if it's not already set (e.g. by controller for specific context like supply product)
|
$snapshot['name'] = $this->name;
|
||||||
if (!isset($snapshot['name'])) {
|
|
||||||
$snapshot['name'] = $this->name;
|
|
||||||
}
|
|
||||||
$properties['snapshot'] = $snapshot;
|
$properties['snapshot'] = $snapshot;
|
||||||
|
|
||||||
$activity->properties = $properties;
|
$activity->properties = $properties;
|
||||||
38
app/Modules/Procurement/Routes/web.php
Normal file
38
app/Modules/Procurement/Routes/web.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Modules\Procurement\Controllers\VendorController;
|
||||||
|
use App\Modules\Procurement\Controllers\VendorProductController;
|
||||||
|
use App\Modules\Procurement\Controllers\PurchaseOrderController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Modules\Production\Controllers;
|
||||||
|
|
||||||
use App\Models\Inventory;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Product;
|
|
||||||
use App\Models\ProductionOrder;
|
use App\Modules\Inventory\Models\Product;
|
||||||
use App\Models\ProductionOrderItem;
|
use App\Modules\Production\Models\ProductionOrder;
|
||||||
use App\Models\Unit;
|
use App\Modules\Production\Models\ProductionOrderItem;
|
||||||
use App\Models\Warehouse;
|
use App\Modules\Inventory\Models\Unit;
|
||||||
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
70
app/Modules/Production/Models/ProductionOrder.php
Normal file
70
app/Modules/Production/Models/ProductionOrder.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Production\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Modules\Inventory\Models\Product;
|
||||||
|
use App\Modules\Inventory\Models\Warehouse;
|
||||||
|
use App\Modules\Core\Models\User;
|
||||||
|
|
||||||
|
class ProductionOrder extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\ProductionOrderFactory> */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Modules/Production/Models/ProductionOrderItem.php
Normal file
44
app/Modules/Production/Models/ProductionOrderItem.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Modules\Production\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Modules\Inventory\Models\Product;
|
||||||
|
|
||||||
|
class ProductionOrderItem extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\ProductionOrderItemFactory> */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Modules/Production/Routes/web.php
Normal file
28
app/Modules/Production/Routes/web.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Modules\Production\Controllers\ProductionOrderController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->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');
|
||||||
|
});
|
||||||
40
app/Providers/ModuleServiceProvider.php
Normal file
40
app/Providers/ModuleServiceProvider.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class ModuleServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$modulesPath = app_path('Modules');
|
||||||
|
|
||||||
|
if (File::exists($modulesPath)) {
|
||||||
|
$modules = File::directories($modulesPath);
|
||||||
|
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
// $moduleName = basename($module);
|
||||||
|
$routesPath = $module . '/Routes/web.php';
|
||||||
|
|
||||||
|
if (File::exists($routesPath)) {
|
||||||
|
Route::middleware('web')
|
||||||
|
->group($routesPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
App\Providers\TenancyServiceProvider::class,
|
App\Providers\TenancyServiceProvider::class,
|
||||||
|
App\Providers\ModuleServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
|
"App\\Modules\\": "app/Modules/",
|
||||||
"Database\\Factories\\": "database/factories/",
|
"Database\\Factories\\": "database/factories/",
|
||||||
"Database\\Seeders\\": "database/seeders/"
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ return [
|
|||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
'model' => env('AUTH_MODEL', App\Modules\Core\Models\User::class),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ return [
|
|||||||
* `Spatie\Permission\Contracts\Role` contract.
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'role' => App\Models\Role::class,
|
'role' => App\Modules\Core\Models\Role::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Stancl\Tenancy\Database\Models\Domain;
|
use Stancl\Tenancy\Database\Models\Domain;
|
||||||
use App\Models\Tenant;
|
use App\Modules\Core\Models\Tenant;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant_model' => Tenant::class,
|
'tenant_model' => Tenant::class,
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\Category;
|
use App\Modules\Inventory\Models\Category;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
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
|
class ProductFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Hash;
|
|||||||
use Illuminate\Support\Str;
|
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
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Category;
|
use App\Modules\Inventory\Models\Category;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class CategorySeeder extends Seeder
|
class CategorySeeder extends Seeder
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Modules\Core\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Database\Seeders;
|
|||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
use App\Models\User;
|
use App\Modules\Core\Models\User;
|
||||||
|
|
||||||
class PermissionSeeder extends Seeder
|
class PermissionSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Category;
|
use App\Modules\Inventory\Models\Category;
|
||||||
use App\Models\Product;
|
use App\Modules\Inventory\Models\Product;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class ProductSeeder extends Seeder
|
class ProductSeeder extends Seeder
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use App\Models\User;
|
use App\Modules\Core\Models\User;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Unit;
|
use App\Modules\Inventory\Models\Unit;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class UnitSeeder extends Seeder
|
class UnitSeeder extends Seeder
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Vendor;
|
use App\Modules\Procurement\Models\Vendor;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class VendorSeeder extends Seeder
|
class VendorSeeder extends Seeder
|
||||||
|
|||||||
60
docs/FRAMEWORK_SPEC.md
Normal file
60
docs/FRAMEWORK_SPEC.md
Normal file
@@ -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`
|
||||||
92
docs/MODULAR_ARCHITECTURE.md
Normal file
92
docs/MODULAR_ARCHITECTURE.md
Normal file
@@ -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**| 化妝品 | 成分分析、過敏原管理 |
|
||||||
@@ -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();
|
return num.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化貨幣(NT$)
|
* 格式化貨幣(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()}`;
|
return `NT$ ${num.toLocaleString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
188
routes/web.php
188
routes/web.php
@@ -2,211 +2,37 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
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;
|
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::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
|
}); // End of auth middleware group
|
||||||
|
|||||||
Reference in New Issue
Block a user