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
|
||||
---
|
||||
預設專案運行於 WSL2 的 Laravel Sail (Docker) 環境。
|
||||
開發框架規範說明書:ERP 系統 (koori-erp)
|
||||
1. 專案概述
|
||||
目標: 打造一個強大且穩定的 ERP 後台管理系統。
|
||||
|
||||
核心架構: 採用 單體式架構配現代化前端 (Monolith with a Modern Frontend)。使用 Laravel、Inertia.js 及 React。
|
||||
# 開發框架規範說明書:ERP 系統 (star-erp)
|
||||
|
||||
工作流程: 將 UI/UX 設計師提供的 React 原始碼,透過 Inertia.js 整合進 Laravel 環境中。
|
||||
## 1. 專案概述
|
||||
* **目標**: 打造一個強大且穩定的 ERP 後台管理系統。
|
||||
* **核心架構**: 採用 **模組化單體式架構 (Modular Monolith)** 配現代化前端。使用 Laravel、Inertia.js 及 React。
|
||||
* **工作流程**: 將 UI/UX 設計師提供的 React 原始碼,透過 Inertia.js 整合進 Laravel 環境中。後端邏輯依據「業務領域」拆分為獨立模組。
|
||||
|
||||
2. 技術棧 (Tech Stack)
|
||||
後端: PHP 8.5 / Laravel 12
|
||||
## 2. 技術棧 (Tech Stack)
|
||||
* **後端**: PHP 8.5 / Laravel 12
|
||||
* **前端橋樑**: Inertia.js (不使用傳統 RESTful API 串接頁面,改用 Inertia 協議)
|
||||
* **前端庫**: React (以 Functional Components 與 Hooks 為主)
|
||||
* **樣式處理**: Tailwind CSS (確保與 UI/UX 設計稿完全一致)
|
||||
* **資料庫**: MySQL 8.0
|
||||
* **開發環境**: Laravel Sail (Docker / WSL2)
|
||||
* **未來擴充**: 針對高併發或跨平台模組,預留 Golang 微服務接口。
|
||||
|
||||
前端橋樑: Inertia.js (不使用傳統 RESTful API 串接頁面,改用 Inertia 協議)
|
||||
## 3. 目錄結構與慣例
|
||||
|
||||
前端庫: React (以 Functional Components 與 Hooks 為主)
|
||||
### 3.1 後端 (Laravel - Modular Monolith)
|
||||
系統採用模組化架構,核心邏輯位於 `app/Modules/` 下:
|
||||
|
||||
樣式處理: Tailwind CSS (確保與 UI/UX 設計稿完全一致)
|
||||
* **Modules**: 位於 `app/Modules/{ModuleName}/`。
|
||||
* **Controllers**: `app/Modules/{ModuleName}/Controllers/`。必須回傳 `Inertia::render()`。
|
||||
* **Models**: `app/Modules/{ModuleName}/Models/`。
|
||||
* **Routes**: `app/Modules/{ModuleName}/Routes/web.php`。各模組獨立管理路由。
|
||||
* **Global Routes**: `routes/web.php` 僅保留全域通用路由或作為模組路由的載入點。
|
||||
|
||||
資料庫: MySQL 8.0
|
||||
### 3.2 前端 (React)
|
||||
* **Pages (頁面)**: 位於 `resources/js/Pages/`。每個檔案代表一個完整的路由視圖。
|
||||
* **Components (組件)**: 位於 `resources/js/Components/`。存放由 UI/UX 團隊提供的可重複使用 UI 元件。
|
||||
* **Layouts (版面)**: 位於 `resources/js/Layouts/`。定義 ERP 的通用版面。
|
||||
|
||||
開發環境: Laravel Sail (Docker / WSL2)
|
||||
## 4. 整合指南 (UI/UX 轉換至 Laravel)
|
||||
* **組件遷移**: 將 UI/UX 的 React 原始碼移入 `resources/js/` 時,應進行「原子化」拆解,提高元件複用率。
|
||||
* **資料傳遞**: 透過 Laravel Controller 的 props 傳送動態資料給 React。優先使用 Inertia 資料流,避免初次渲染時使用 axios。
|
||||
* **狀態管理**: 優先使用 Inertia 內建的狀態管理與 useForm hook 處理表單提交。
|
||||
|
||||
未來擴充: 針對高併發或跨平台模組,預留 Golang 微服務接口。
|
||||
## 5. 開發標準 (Coding Standards)
|
||||
* **命名規範**:
|
||||
* Controllers: `PascalCaseController.php`
|
||||
* React Components: `PascalCase.jsx`
|
||||
* Routes: `kebab-case` (小寫橫線分隔)
|
||||
* **回傳格式**: 所有的前後端溝通需維持一致的 JSON 結構,特別是驗證錯誤 (Validation Errors) 與閃存訊息 (Flash Messages)。
|
||||
|
||||
3. 目錄結構與慣例
|
||||
3.1 後端 (Laravel)
|
||||
Controllers: 必須回傳 Inertia::render() 來渲染頁面。
|
||||
## 6. AI 協作規則 (給 Antigravity AI)
|
||||
* **角色設定**: 你是一位專業的全端開發工程師助手。
|
||||
* **代碼生成指令**:
|
||||
* 所有的解釋說明請使用 **繁體中文**。
|
||||
* 生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。
|
||||
* 必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。
|
||||
* 新增功能時,請先判斷應歸屬於哪個 Module,並建立在 `app/Modules/` 對應目錄下。
|
||||
|
||||
Models: 嚴格執行型別標註,使用 Eloquent 進行資料庫操作。
|
||||
## 7. 運行機制 (Docker / Sail)
|
||||
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
|
||||
|
||||
Routes: 統一在 routes/web.php 定義 Inertia 路由。
|
||||
|
||||
3.2 前端 (React)
|
||||
Pages (頁面): 位於 resources/js/Pages/。每個檔案代表一個完整的路由視圖。
|
||||
|
||||
Components (組件): 位於 resources/js/Components/。存放由 UI/UX 團隊提供的可重複使用 UI 元件。
|
||||
|
||||
Layouts (版面): 位於 resources/js/Layouts/。定義 ERP 的通用版面(例如:包含側邊欄 Sidebar 與導覽列 Navbar 的後台主框架)。
|
||||
|
||||
4. 整合指南 (UI/UX 轉換至 Laravel)
|
||||
組件遷移: 將 UI/UX 的 React 原始碼移入 resources/js/ 時,應進行「原子化」拆解,提高元件複用率。
|
||||
|
||||
資料傳遞: 透過 Laravel Controller 的 props 傳送動態資料給 React。除非是後續的異步請求,否則避免在 React 初次渲染時使用 axios 抓取資料,應優先使用 Inertia 的資料流。
|
||||
|
||||
狀態管理: 優先使用 Inertia 內建的狀態管理與 useForm hook 處理表單提交。
|
||||
|
||||
5. 開發標準 (Coding Standards)
|
||||
命名規範:
|
||||
|
||||
Controllers: PascalCaseController.php
|
||||
|
||||
React Components: PascalCase.jsx
|
||||
|
||||
Routes: kebab-case (小寫橫線分隔)
|
||||
|
||||
回傳格式: 所有的前後端溝通需維持一致的 JSON 結構,特別是驗證錯誤 (Validation Errors) 與閃存訊息 (Flash Messages)。
|
||||
|
||||
6. AI 協作規則 (給 Antigravity AI)
|
||||
角色設定: 你是一位專業的全端開發工程師助手。
|
||||
|
||||
代碼生成指令:
|
||||
|
||||
所有的解釋說明請使用 繁體中文。
|
||||
|
||||
生成 React 組件時,必須符合專案現有的 Tailwind CSS 配置。
|
||||
|
||||
必須考慮 ERP 邏輯(例如:權限判斷、操作日誌、資料完整性)。
|
||||
|
||||
7.運行機制
|
||||
因為是運行在docker上 所以要執行php的話 要執行docker exce
|
||||
* **啟動環境**: `./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`
|
||||
@@ -101,8 +101,8 @@ public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, st
|
||||
protected function getSubjectMap()
|
||||
{
|
||||
return [
|
||||
'App\Models\Product' => '商品',
|
||||
'App\Models\UtilityFee' => '公共事業費', // ✅ 新增映射
|
||||
'App\Modules\Inventory\Models\Product' => '商品',
|
||||
'App\Modules\Finance\Models\UtilityFee' => '公共事業費', // ✅ 新增映射
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Modules\Core\Models\Tenant;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Landlord;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenant;
|
||||
use App\Modules\Core\Models\Tenant;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class DashboardController extends Controller
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Landlord;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenant;
|
||||
use App\Modules\Core\Models\Tenant;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
@@ -12,16 +13,16 @@ class ActivityLogController extends Controller
|
||||
private function getSubjectMap()
|
||||
{
|
||||
return [
|
||||
'App\Models\User' => '使用者',
|
||||
'App\Models\Role' => '角色',
|
||||
'App\Models\Product' => '商品',
|
||||
'App\Models\Vendor' => '廠商',
|
||||
'App\Models\Category' => '商品分類',
|
||||
'App\Models\Unit' => '單位',
|
||||
'App\Models\PurchaseOrder' => '採購單',
|
||||
'App\Models\Warehouse' => '倉庫',
|
||||
'App\Models\Inventory' => '庫存',
|
||||
'App\Models\UtilityFee' => '公共事業費',
|
||||
'App\Modules\Core\Models\User' => '使用者',
|
||||
'App\Modules\Core\Models\Role' => '角色',
|
||||
'App\Modules\Inventory\Models\Product' => '商品',
|
||||
'App\Modules\Procurement\Models\Vendor' => '廠商',
|
||||
'App\Modules\Inventory\Models\Category' => '商品分類',
|
||||
'App\Modules\Inventory\Models\Unit' => '單位',
|
||||
'App\Modules\Procurement\Models\PurchaseOrder' => '採購單',
|
||||
'App\Modules\Inventory\Models\Warehouse' => '倉庫',
|
||||
'App\Modules\Inventory\Models\Inventory' => '庫存',
|
||||
'App\Modules\Finance\Models\UtilityFee' => '公共事業費',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ class ActivityLogController extends Controller
|
||||
})->values();
|
||||
|
||||
// Get users for causer filter
|
||||
$users = \App\Models\User::select('id', 'name')->orderBy('name')->get()
|
||||
$users = \App\Modules\Core\Models\User::select('id', 'name')->orderBy('name')->get()
|
||||
->map(function ($user) {
|
||||
return ['label' => $user->name, 'value' => (string) $user->id];
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
namespace App\Modules\Core\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Warehouse;
|
||||
use App\Models\Inventory;
|
||||
use App\Models\WarehouseProductSafetyStock;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Procurement\Models\Vendor;
|
||||
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use App\Modules\Inventory\Models\Inventory;
|
||||
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -1,8 +1,9 @@
|
||||
<?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\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
namespace App\Modules\Core\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
|
||||
use App\Modules\Core\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Inertia\Inertia;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Core\Models;
|
||||
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Core\Models;
|
||||
|
||||
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
|
||||
use Stancl\Tenancy\Contracts\TenantWithDatabase;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Core\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
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
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Finance\Controllers;
|
||||
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\UtilityFee;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Finance\Models\UtilityFee;
|
||||
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -1,8 +1,10 @@
|
||||
<?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 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
|
||||
|
||||
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;
|
||||
|
||||
class CategoryController extends Controller
|
||||
@@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Inventory\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\WarehouseProductSafetyStock;
|
||||
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||
|
||||
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([
|
||||
'inventories.product.category',
|
||||
@@ -15,7 +17,7 @@ class InventoryController extends Controller
|
||||
'inventories.lastIncomingTransaction',
|
||||
'inventories.lastOutgoingTransaction'
|
||||
]);
|
||||
$allProducts = \App\Models\Product::with('category')->get();
|
||||
$allProducts = \App\Modules\Inventory\Models\Product::with('category')->get();
|
||||
|
||||
// 1. 準備 availableProducts
|
||||
$availableProducts = $allProducts->map(function ($product) {
|
||||
@@ -104,10 +106,10 @@ class InventoryController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(\App\Models\Warehouse $warehouse)
|
||||
public function create(\App\Modules\Inventory\Models\Warehouse $warehouse)
|
||||
{
|
||||
// 取得所有商品供前端選單使用
|
||||
$products = \App\Models\Product::with(['baseUnit', 'largeUnit'])
|
||||
$products = \App\Modules\Inventory\Models\Product::with(['baseUnit', 'largeUnit'])
|
||||
->select('id', 'name', 'code', 'base_unit_id', 'large_unit_id', 'conversion_rate')
|
||||
->get()
|
||||
->map(function ($product) {
|
||||
@@ -127,7 +129,7 @@ class InventoryController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse)
|
||||
public function store(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'inboundDate' => 'required|date',
|
||||
@@ -148,16 +150,16 @@ class InventoryController extends Controller
|
||||
|
||||
if ($item['batchMode'] === 'existing') {
|
||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||
$inventory = \App\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
$inventory = \App\Modules\Inventory\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
$product = \App\Models\Product::find($item['productId']);
|
||||
$product = \App\Modules\Inventory\Models\Product::find($item['productId']);
|
||||
|
||||
$batchNumber = \App\Models\Inventory::generateBatchNumber(
|
||||
$batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$validated['inboundDate']
|
||||
@@ -208,12 +210,12 @@ class InventoryController extends Controller
|
||||
/**
|
||||
* API: 取得商品在特定倉庫的所有批號,並回傳當前日期/產地下的一個流水號
|
||||
*/
|
||||
public function getBatches(\App\Models\Warehouse $warehouse, $productId, \Illuminate\Http\Request $request)
|
||||
public function getBatches(\App\Modules\Inventory\Models\Warehouse $warehouse, $productId, \Illuminate\Http\Request $request)
|
||||
{
|
||||
$originCountry = $request->query('originCountry', 'TW');
|
||||
$arrivalDate = $request->query('arrivalDate', now()->format('Y-m-d'));
|
||||
|
||||
$batches = \App\Models\Inventory::where('warehouse_id', $warehouse->id)
|
||||
$batches = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $warehouse->id)
|
||||
->where('product_id', $productId)
|
||||
->get()
|
||||
->map(function ($inventory) {
|
||||
@@ -227,10 +229,10 @@ class InventoryController extends Controller
|
||||
});
|
||||
|
||||
// 計算下一個流水號
|
||||
$product = \App\Models\Product::find($productId);
|
||||
$product = \App\Modules\Inventory\Models\Product::find($productId);
|
||||
$nextSequence = '01';
|
||||
if ($product) {
|
||||
$batchNumber = \App\Models\Inventory::generateBatchNumber(
|
||||
$batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$arrivalDate
|
||||
@@ -244,7 +246,7 @@ class InventoryController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId)
|
||||
public function edit(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
// 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人)
|
||||
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
|
||||
@@ -252,7 +254,7 @@ class InventoryController extends Controller
|
||||
return redirect()->back()->with('error', '無法編輯範例資料');
|
||||
}
|
||||
|
||||
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$inventory = \App\Modules\Inventory\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||
|
||||
@@ -289,13 +291,13 @@ class InventoryController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId)
|
||||
public function update(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
// 若是 product ID (舊邏輯),先轉為 inventory
|
||||
// 但新路由我們傳的是 inventory ID
|
||||
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
|
||||
|
||||
$inventory = \App\Models\Inventory::find($inventoryId);
|
||||
$inventory = \App\Modules\Inventory\Models\Inventory::find($inventoryId);
|
||||
|
||||
// 如果找不到 (可能是舊路由傳 product ID)
|
||||
if (!$inventory) {
|
||||
@@ -393,9 +395,9 @@ class InventoryController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||
public function destroy(\App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
$inventory = \App\Models\Inventory::findOrFail($inventoryId);
|
||||
$inventory = \App\Modules\Inventory\Models\Inventory::findOrFail($inventoryId);
|
||||
|
||||
// 庫存 > 0 不允許刪除 (哪怕是軟刪除)
|
||||
if ($inventory->quantity > 0) {
|
||||
@@ -421,14 +423,14 @@ class InventoryController extends Controller
|
||||
->with('success', '庫存品項已刪除');
|
||||
}
|
||||
|
||||
public function history(Request $request, \App\Models\Warehouse $warehouse)
|
||||
public function history(Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse)
|
||||
{
|
||||
$inventoryId = $request->query('inventoryId');
|
||||
$productId = $request->query('productId');
|
||||
|
||||
if ($productId) {
|
||||
// 商品層級查詢
|
||||
$inventories = \App\Models\Inventory::where('warehouse_id', $warehouse->id)
|
||||
$inventories = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $warehouse->id)
|
||||
->where('product_id', $productId)
|
||||
->with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
@@ -503,7 +505,7 @@ class InventoryController extends Controller
|
||||
|
||||
if ($inventoryId) {
|
||||
// 單一批號查詢
|
||||
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$inventory = \App\Modules\Inventory\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Inventory\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Unit;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Inventory\Models\Unit;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -59,7 +61,7 @@ class ProductController extends Controller
|
||||
|
||||
$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', [
|
||||
'products' => $products,
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Inventory\Controllers;
|
||||
|
||||
use App\Models\Warehouse;
|
||||
use App\Models\WarehouseProductSafetyStock;
|
||||
use App\Models\Product;
|
||||
use App\Models\Inventory;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Inventory\Models\Inventory;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Inventory\Controllers;
|
||||
|
||||
use App\Models\Inventory;
|
||||
use App\Models\Warehouse;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Inventory;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Inventory\Controllers;
|
||||
|
||||
use App\Models\Unit;
|
||||
use App\Models\Product; // Import Product to check for usage
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Unit;
|
||||
use App\Modules\Inventory\Models\Product; // Import Product to check for usage
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UnitController extends Controller
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Inventory\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use App\Models\Warehouse;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use HasFactory, LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'is_active',
|
||||
];
|
||||
protected $fillable = ['name', 'description'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the products for the category.
|
||||
*/
|
||||
public function products(): HasMany
|
||||
{
|
||||
return $this->hasMany(Product::class);
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return \Spatie\Activitylog\LogOptions::defaults()
|
||||
return LogOptions::defaults()
|
||||
->logAll()
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Modules\Procurement\Models\PurchaseOrder; // Cross-module dependency
|
||||
|
||||
class Inventory extends Model
|
||||
{
|
||||
@@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\Inventory;
|
||||
use App\Models\User;
|
||||
use App\Modules\Core\Models\User; // Cross-module Core dependency
|
||||
|
||||
class InventoryTransaction extends Model
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use App\Modules\Procurement\Models\Vendor; // Cross-module dependency (Procurement)
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
@@ -1,24 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Unit extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UnitFactory> */
|
||||
use HasFactory, LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
];
|
||||
protected $fillable = ['name', 'abbreviation'];
|
||||
|
||||
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
||||
public function productsAsBase(): HasMany
|
||||
{
|
||||
return \Spatie\Activitylog\LogOptions::defaults()
|
||||
return $this->hasMany(Product::class, 'base_unit_id');
|
||||
}
|
||||
|
||||
public function productsAsLarge(): HasMany
|
||||
{
|
||||
return $this->hasMany(Product::class, 'large_unit_id');
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logAll()
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Modules\Procurement\Models\PurchaseOrder; // Cross-module dependency (Procurement)
|
||||
|
||||
class Warehouse extends Model
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Inventory\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
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
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Procurement\Controllers;
|
||||
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\Warehouse;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||
use App\Modules\Procurement\Models\Vendor;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -146,9 +148,9 @@ class PurchaseOrderController extends Controller
|
||||
// 確保有一個有效的使用者 ID
|
||||
$userId = auth()->id();
|
||||
if (!$userId) {
|
||||
$user = \App\Models\User::first();
|
||||
$user = \App\Modules\Core\Models\User::first();
|
||||
if (!$user) {
|
||||
$user = \App\Models\User::create([
|
||||
$user = \App\Modules\Core\Models\User::create([
|
||||
'name' => '系統管理員',
|
||||
'email' => 'admin@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
@@ -1,8 +1,10 @@
|
||||
<?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;
|
||||
|
||||
class VendorController extends Controller
|
||||
@@ -56,7 +58,7 @@ class VendorController extends Controller
|
||||
$vendor->load(['products.baseUnit', 'products.largeUnit']);
|
||||
return \Inertia\Inertia::render('Vendor/Show', [
|
||||
'vendor' => $vendor,
|
||||
'products' => \App\Models\Product::with('baseUnit')->get(),
|
||||
'products' => \App\Modules\Inventory\Models\Product::with('baseUnit')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?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\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()
|
||||
->performedOn($vendor)
|
||||
->withProperties([
|
||||
@@ -66,7 +68,7 @@ class VendorProductController extends Controller
|
||||
]);
|
||||
|
||||
// 記錄操作
|
||||
$product = \App\Models\Product::find($productId);
|
||||
$product = \App\Modules\Inventory\Models\Product::find($productId);
|
||||
activity()
|
||||
->performedOn($vendor)
|
||||
->withProperties([
|
||||
@@ -95,7 +97,7 @@ class VendorProductController extends Controller
|
||||
public function destroy(Vendor $vendor, $productId)
|
||||
{
|
||||
// 記錄操作 (需在 detach 前獲取資訊)
|
||||
$product = \App\Models\Product::find($productId);
|
||||
$product = \App\Modules\Inventory\Models\Product::find($productId);
|
||||
$old_price = $vendor->products()->where('product_id', $productId)->first()?->pivot?->last_price;
|
||||
|
||||
$vendor->products()->detach($productId);
|
||||
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
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Modules\Procurement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
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\LogOptions;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
|
||||
class Vendor extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
/** @use HasFactory<\Database\Factories\VendorFactory> */
|
||||
use HasFactory, LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'short_name',
|
||||
'tax_id',
|
||||
'owner',
|
||||
'contact_name',
|
||||
'tel',
|
||||
'phone',
|
||||
'contact_person',
|
||||
'email',
|
||||
'phone',
|
||||
'address',
|
||||
'remark'
|
||||
'tax_id',
|
||||
'payment_terms',
|
||||
];
|
||||
public function products(): BelongsToMany
|
||||
|
||||
public function products(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'product_vendor')
|
||||
->withPivot('last_price')
|
||||
->withTimestamps();
|
||||
return $this->belongsToMany(Product::class)->withPivot('last_price')->withTimestamps();
|
||||
}
|
||||
|
||||
public function purchaseOrders(): HasMany
|
||||
public function purchaseOrders(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseOrder::class);
|
||||
}
|
||||
@@ -49,12 +46,8 @@ class Vendor extends Model
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
|
||||
// Store name in 'snapshot' for context, keeping 'attributes' clean
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
// Only set name if it's not already set (e.g. by controller for specific context like supply product)
|
||||
if (!isset($snapshot['name'])) {
|
||||
$snapshot['name'] = $this->name;
|
||||
}
|
||||
$snapshot['name'] = $this->name;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
$activity->properties = $properties;
|
||||
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
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Modules\Production\Controllers;
|
||||
|
||||
use App\Models\Inventory;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductionOrder;
|
||||
use App\Models\ProductionOrderItem;
|
||||
use App\Models\Unit;
|
||||
use App\Models\Warehouse;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Production\Models\ProductionOrder;
|
||||
use App\Modules\Production\Models\ProductionOrderItem;
|
||||
use App\Modules\Inventory\Models\Unit;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
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 [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\TenancyServiceProvider::class,
|
||||
App\Providers\ModuleServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"App\\Modules\\": "app/Modules/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
@@ -92,4 +93,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ return [
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', App\Modules\Core\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
@@ -24,7 +24,7 @@ return [
|
||||
* `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);
|
||||
|
||||
use Stancl\Tenancy\Database\Models\Domain;
|
||||
use App\Models\Tenant;
|
||||
use App\Modules\Core\Models\Tenant;
|
||||
|
||||
return [
|
||||
'tenant_model' => Tenant::class,
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Modules\Inventory\Models\Category;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Modules\Inventory\Models\Product>
|
||||
*/
|
||||
class ProductFactory extends Factory
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Modules\Core\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Modules\Inventory\Models\Category;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CategorySeeder extends Seeder
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Core\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Database\Seeders;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use App\Models\User;
|
||||
use App\Modules\Core\Models\User;
|
||||
|
||||
class PermissionSeeder extends Seeder
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use App\Modules\Inventory\Models\Category;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ProductSeeder extends Seeder
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\User;
|
||||
use App\Modules\Core\Models\User;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Unit;
|
||||
use App\Modules\Inventory\Models\Unit;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class UnitSeeder extends Seeder
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Vendor;
|
||||
use App\Modules\Procurement\Models\Vendor;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class VendorSeeder extends Seeder
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化貨幣(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()}`;
|
||||
};
|
||||
|
||||
|
||||
188
routes/web.php
188
routes/web.php
@@ -2,211 +2,37 @@
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use App\Http\Controllers\CategoryController;
|
||||
use App\Http\Controllers\VendorController;
|
||||
use App\Http\Controllers\VendorProductController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\PurchaseOrderController;
|
||||
use App\Http\Controllers\WarehouseController;
|
||||
use App\Http\Controllers\InventoryController;
|
||||
use App\Http\Controllers\SafetyStockController;
|
||||
use App\Http\Controllers\TransferOrderController;
|
||||
use App\Http\Controllers\UnitController;
|
||||
use App\Http\Controllers\Admin\RoleController;
|
||||
use App\Http\Controllers\Admin\UserController;
|
||||
use App\Http\Controllers\Admin\ActivityLogController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\UtilityFeeController;
|
||||
use App\Http\Controllers\AccountingReportController;
|
||||
use App\Http\Controllers\ProductionOrderController;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||
|
||||
// 登入/登出路由
|
||||
Route::get('/login', [LoginController::class, 'show'])->name('login');
|
||||
Route::post('/login', [LoginController::class, 'store']);
|
||||
Route::post('/logout', [LoginController::class, 'destroy'])->name('logout');
|
||||
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
// 儀表板 - 所有登入使用者皆可存取
|
||||
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
// 使用者帳號設定
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
|
||||
|
||||
// 類別管理 (用於商品對話框) - 需要商品權限
|
||||
Route::middleware('permission:products.view')->group(function () {
|
||||
Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index');
|
||||
Route::post('/categories', [CategoryController::class, 'store'])->middleware('permission:products.create')->name('categories.store');
|
||||
Route::put('/categories/{category}', [CategoryController::class, 'update'])->middleware('permission:products.edit')->name('categories.update');
|
||||
Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->middleware('permission:products.delete')->name('categories.destroy');
|
||||
});
|
||||
|
||||
// 單位管理 - 需要商品權限
|
||||
Route::middleware('permission:products.create|products.edit')->group(function () {
|
||||
Route::post('/units', [UnitController::class, 'store'])->name('units.store');
|
||||
Route::put('/units/{unit}', [UnitController::class, 'update'])->name('units.update');
|
||||
Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->name('units.destroy');
|
||||
});
|
||||
|
||||
// 商品管理
|
||||
Route::middleware('permission:products.view')->group(function () {
|
||||
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
|
||||
Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store');
|
||||
Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update');
|
||||
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->middleware('permission:products.delete')->name('products.destroy');
|
||||
});
|
||||
|
||||
// 廠商管理
|
||||
Route::middleware('permission:vendors.view')->group(function () {
|
||||
Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index');
|
||||
Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show');
|
||||
Route::post('/vendors', [VendorController::class, 'store'])->middleware('permission:vendors.create')->name('vendors.store');
|
||||
Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.update');
|
||||
Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->middleware('permission:vendors.delete')->name('vendors.destroy');
|
||||
|
||||
// 供貨商品相關路由
|
||||
Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->middleware('permission:vendors.edit')->name('vendors.products.store');
|
||||
Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.products.update');
|
||||
Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->middleware('permission:vendors.edit')->name('vendors.products.destroy');
|
||||
});
|
||||
|
||||
// 倉庫管理
|
||||
Route::middleware('permission:warehouses.view')->group(function () {
|
||||
Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index');
|
||||
Route::post('/warehouses', [WarehouseController::class, 'store'])->middleware('permission:warehouses.create')->name('warehouses.store');
|
||||
Route::put('/warehouses/{warehouse}', [WarehouseController::class, 'update'])->middleware('permission:warehouses.edit')->name('warehouses.update');
|
||||
Route::delete('/warehouses/{warehouse}', [WarehouseController::class, 'destroy'])->middleware('permission:warehouses.delete')->name('warehouses.destroy');
|
||||
|
||||
// 倉庫庫存管理 - 需要庫存權限
|
||||
Route::middleware('permission:inventory.view')->group(function () {
|
||||
Route::get('/warehouses/{warehouse}/inventory', [InventoryController::class, 'index'])->name('warehouses.inventory.index');
|
||||
Route::get('/warehouses/{warehouse}/inventory-history', [InventoryController::class, 'history'])->name('warehouses.inventory.history');
|
||||
|
||||
Route::middleware('permission:inventory.adjust')->group(function () {
|
||||
Route::get('/warehouses/{warehouse}/inventory/create', [InventoryController::class, 'create'])->name('warehouses.inventory.create');
|
||||
Route::post('/warehouses/{warehouse}/inventory', [InventoryController::class, 'store'])->name('warehouses.inventory.store');
|
||||
Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/edit', [InventoryController::class, 'edit'])->name('warehouses.inventory.edit');
|
||||
Route::put('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'update'])->name('warehouses.inventory.update');
|
||||
Route::delete('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy');
|
||||
});
|
||||
|
||||
// API: 取得商品在特定倉庫的所有批號
|
||||
Route::get('/api/warehouses/{warehouse}/inventory/batches/{productId}', [InventoryController::class, 'getBatches'])
|
||||
->name('api.warehouses.inventory.batches');
|
||||
});
|
||||
|
||||
// 安全庫存設定
|
||||
Route::middleware('permission:inventory.view')->group(function () {
|
||||
Route::get('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'index'])->name('warehouses.safety-stock.index');
|
||||
Route::middleware('permission:inventory.safety_stock')->group(function () {
|
||||
Route::post('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'store'])->name('warehouses.safety-stock.store');
|
||||
Route::put('/warehouses/{warehouse}/safety-stock/{safetyStock}', [SafetyStockController::class, 'update'])->name('warehouses.safety-stock.update');
|
||||
Route::delete('/warehouses/{warehouse}/safety-stock/{safetyStock}', [SafetyStockController::class, 'destroy'])->name('warehouses.safety-stock.destroy');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 採購單管理
|
||||
Route::middleware('permission:purchase_orders.view')->group(function () {
|
||||
Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index');
|
||||
|
||||
Route::middleware('permission:purchase_orders.create')->group(function () {
|
||||
Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create');
|
||||
Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store');
|
||||
});
|
||||
|
||||
Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show');
|
||||
|
||||
Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.edit');
|
||||
Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.update');
|
||||
Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->middleware('permission:purchase_orders.delete')->name('purchase-orders.destroy');
|
||||
});
|
||||
|
||||
// 公共事業費管理 (TODO: 添加權限控制)
|
||||
// 公共事業費
|
||||
Route::middleware('permission:utility_fees.view')->group(function () {
|
||||
Route::get('/utility-fees', [UtilityFeeController::class, 'index'])->name('utility-fees.index');
|
||||
});
|
||||
Route::middleware('permission:utility_fees.create')->group(function () {
|
||||
Route::post('/utility-fees', [UtilityFeeController::class, 'store'])->name('utility-fees.store');
|
||||
});
|
||||
Route::middleware('permission:utility_fees.edit')->group(function () {
|
||||
Route::put('/utility-fees/{utility_fee}', [UtilityFeeController::class, 'update'])->name('utility-fees.update');
|
||||
});
|
||||
Route::middleware('permission:utility_fees.delete')->group(function () {
|
||||
Route::delete('/utility-fees/{utility_fee}', [UtilityFeeController::class, 'destroy'])->name('utility-fees.destroy');
|
||||
});
|
||||
|
||||
// 撥補單 (在庫存調撥時使用)
|
||||
Route::middleware('permission:inventory.transfer')->group(function () {
|
||||
Route::post('/transfer-orders', [TransferOrderController::class, 'store'])->name('transfer-orders.store');
|
||||
});
|
||||
Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories'])
|
||||
->middleware('permission:inventory.view')
|
||||
->name('api.warehouses.inventories');
|
||||
|
||||
// 系統管理
|
||||
Route::middleware('permission:accounting.view')->prefix('accounting-report')->group(function () {
|
||||
Route::get('/', [AccountingReportController::class, 'index'])->name('accounting.report');
|
||||
Route::get('/export', [AccountingReportController::class, 'export'])
|
||||
->middleware('permission:accounting.export')
|
||||
->name('accounting.export');
|
||||
});
|
||||
|
||||
// 生產管理
|
||||
Route::middleware('permission:production_orders.view')->group(function () {
|
||||
Route::get('/production-orders', [ProductionOrderController::class, 'index'])->name('production-orders.index');
|
||||
|
||||
Route::middleware('permission:production_orders.create')->group(function () {
|
||||
Route::get('/production-orders/create', [ProductionOrderController::class, 'create'])->name('production-orders.create');
|
||||
Route::post('/production-orders', [ProductionOrderController::class, 'store'])->name('production-orders.store');
|
||||
});
|
||||
|
||||
Route::get('/production-orders/{productionOrder}', [ProductionOrderController::class, 'show'])->name('production-orders.show');
|
||||
|
||||
Route::middleware('permission:production_orders.edit')->group(function () {
|
||||
Route::get('/production-orders/{productionOrder}/edit', [ProductionOrderController::class, 'edit'])->name('production-orders.edit');
|
||||
Route::put('/production-orders/{productionOrder}', [ProductionOrderController::class, 'update'])->name('production-orders.update');
|
||||
});
|
||||
});
|
||||
|
||||
// 生產管理 API
|
||||
Route::get('/api/production/warehouses/{warehouse}/inventories', [ProductionOrderController::class, 'getWarehouseInventories'])
|
||||
->middleware('permission:production_orders.create')
|
||||
->name('api.production.warehouses.inventories');
|
||||
|
||||
// 系統管理
|
||||
Route::prefix('admin')->group(function () {
|
||||
Route::middleware('permission:roles.view')->group(function () {
|
||||
Route::get('/roles', [RoleController::class, 'index'])->name('roles.index');
|
||||
Route::middleware('permission:roles.create')->group(function () {
|
||||
Route::get('/roles/create', [RoleController::class, 'create'])->name('roles.create');
|
||||
Route::post('/roles', [RoleController::class, 'store'])->name('roles.store');
|
||||
});
|
||||
Route::get('/roles/{role}/edit', [RoleController::class, 'edit'])->middleware('permission:roles.edit')->name('roles.edit');
|
||||
Route::put('/roles/{role}', [RoleController::class, 'update'])->middleware('permission:roles.edit')->name('roles.update');
|
||||
Route::delete('/roles/{role}', [RoleController::class, 'destroy'])->middleware('permission:roles.delete')->name('roles.destroy');
|
||||
});
|
||||
|
||||
Route::middleware('permission:users.view')->group(function () {
|
||||
Route::get('/users', [UserController::class, 'index'])->name('users.index');
|
||||
Route::middleware('permission:users.create')->group(function () {
|
||||
Route::get('/users/create', [UserController::class, 'create'])->name('users.create');
|
||||
Route::post('/users', [UserController::class, 'store'])->name('users.store');
|
||||
});
|
||||
Route::get('/users/{user}/edit', [UserController::class, 'edit'])->middleware('permission:users.edit')->name('users.edit');
|
||||
Route::put('/users/{user}', [UserController::class, 'update'])->middleware('permission:users.edit')->name('users.update');
|
||||
Route::delete('/users/{user}', [UserController::class, 'destroy'])->middleware('permission:users.delete')->name('users.destroy');
|
||||
});
|
||||
|
||||
Route::middleware('permission:system.view_logs')->group(function () {
|
||||
Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('activity-logs.index');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}); // End of auth middleware group
|
||||
|
||||
Reference in New Issue
Block a user