chore: 完善模組化架構遷移與修復前端顯示錯誤

- 修正所有模組 Controller 的 Model 引用路徑 (App\Modules\...)
- 更新 ProductionOrder 與 ProductionOrderItem 模型結構以符合新版邏輯
- 修復 resources/js/utils/format.ts 在處理空值時導致 toLocaleString 崩潰的問題
- 清除全域路徑與 Controller 遷移殘留檔案
This commit is contained in:
2026-01-26 10:37:47 +08:00
parent db0c1ce3af
commit b0848a6bb8
70 changed files with 947 additions and 833 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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
View File

View 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];
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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 Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace App\Modules\Core\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View 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');
});
});
});

View File

@@ -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;

View File

@@ -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;

View 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',
];
}

View 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');
});
});

View File

@@ -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

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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();

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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();

View File

@@ -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
{

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace App\Modules\Inventory\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

View 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');
});

View File

@@ -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'),

View File

@@ -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(),
]);
}

View File

@@ -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);

View 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);
}
}

View 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);
}
}

View File

@@ -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;

View 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');
});
});

View File

@@ -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;

View 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);
}
}

View 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);
}
}

View 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');
});

View 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);
}
}
}
}
}