feat: 優化採購單操作紀錄與統一刪除確認 UI
- 優化採購單更新與刪除的活動紀錄邏輯 (PurchaseOrderController) - 整合更新異動為單一紀錄,包含品項差異 - 刪除時記錄當下品項快照 - 統一採購單刪除確認介面,使用 AlertDialog 取代原生 confirm (PurchaseOrderActions) - Refactor: 將 ActivityDetailDialog 移至 Components/ActivityLog 並優化樣式與大數據顯示 - 調整 UI 文字:將「總金額」統一為「小計」 - 其他模型與 Controller 的活動紀錄支援更新
This commit is contained in:
@@ -103,6 +103,7 @@ class PurchaseOrderController extends Controller
|
||||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||
'items.*.subtotal' => 'required|numeric|min:0', // 總金額
|
||||
'items.*.unitId' => 'nullable|exists:units,id',
|
||||
'tax_amount' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
try {
|
||||
@@ -129,8 +130,8 @@ class PurchaseOrderController extends Controller
|
||||
$totalAmount += $item['subtotal'];
|
||||
}
|
||||
|
||||
// Simple tax calculation (e.g., 5%)
|
||||
$taxAmount = round($totalAmount * 0.05, 2);
|
||||
// Tax calculation
|
||||
$taxAmount = isset($validated['tax_amount']) ? $validated['tax_amount'] : round($totalAmount * 0.05, 2);
|
||||
$grandTotal = $totalAmount + $taxAmount;
|
||||
|
||||
// 確保有一個有效的使用者 ID
|
||||
@@ -325,6 +326,9 @@ class PurchaseOrderController extends Controller
|
||||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||
'items.*.subtotal' => 'required|numeric|min:0', // 總金額
|
||||
'items.*.unitId' => 'nullable|exists:units,id',
|
||||
// Allow both tax_amount and taxAmount for compatibility
|
||||
'tax_amount' => 'nullable|numeric|min:0',
|
||||
'taxAmount' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
try {
|
||||
@@ -335,11 +339,13 @@ class PurchaseOrderController extends Controller
|
||||
$totalAmount += $item['subtotal'];
|
||||
}
|
||||
|
||||
// Simple tax calculation (e.g., 5%)
|
||||
$taxAmount = round($totalAmount * 0.05, 2);
|
||||
// Tax calculation (handle both keys)
|
||||
$inputTax = $validated['tax_amount'] ?? $validated['taxAmount'] ?? null;
|
||||
$taxAmount = !is_null($inputTax) ? $inputTax : round($totalAmount * 0.05, 2);
|
||||
$grandTotal = $totalAmount + $taxAmount;
|
||||
|
||||
$order->update([
|
||||
// 1. Fill attributes but don't save yet to capture changes
|
||||
$order->fill([
|
||||
'vendor_id' => $validated['vendor_id'],
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'expected_delivery_date' => $validated['expected_delivery_date'],
|
||||
@@ -353,19 +359,124 @@ class PurchaseOrderController extends Controller
|
||||
'invoice_amount' => $validated['invoice_amount'] ?? null,
|
||||
]);
|
||||
|
||||
// Sync items
|
||||
// Capture attribute changes for manual logging
|
||||
$dirty = $order->getDirty();
|
||||
$oldAttributes = [];
|
||||
$newAttributes = [];
|
||||
|
||||
foreach ($dirty as $key => $value) {
|
||||
$oldAttributes[$key] = $order->getOriginal($key);
|
||||
$newAttributes[$key] = $value;
|
||||
}
|
||||
|
||||
// Save without triggering events (prevents duplicate log)
|
||||
$order->saveQuietly();
|
||||
|
||||
// 2. Capture old items with product names for diffing
|
||||
$oldItems = $order->items()->with('product', 'unit')->get()->map(function($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'product_id' => $item->product_id,
|
||||
'product_name' => $item->product?->name,
|
||||
'quantity' => (float) $item->quantity,
|
||||
'unit_id' => $item->unit_id,
|
||||
'unit_name' => $item->unit?->name,
|
||||
'subtotal' => (float) $item->subtotal,
|
||||
];
|
||||
})->keyBy('product_id');
|
||||
|
||||
// Sync items (Original logic)
|
||||
$order->items()->delete();
|
||||
|
||||
$newItemsData = [];
|
||||
foreach ($validated['items'] as $item) {
|
||||
// 反算單價
|
||||
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
|
||||
|
||||
$order->items()->create([
|
||||
$newItem = $order->items()->create([
|
||||
'product_id' => $item['productId'],
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_id' => $item['unitId'] ?? null,
|
||||
'unit_price' => $unitPrice,
|
||||
'subtotal' => $item['subtotal'],
|
||||
]);
|
||||
$newItemsData[] = $newItem;
|
||||
}
|
||||
|
||||
// 3. Calculate Item Diffs
|
||||
$itemDiffs = [
|
||||
'added' => [],
|
||||
'removed' => [],
|
||||
'updated' => [],
|
||||
];
|
||||
|
||||
// Re-fetch new items to ensure we have fresh relations
|
||||
$newItemsFormatted = $order->items()->with('product', 'unit')->get()->map(function($item) {
|
||||
return [
|
||||
'product_id' => $item->product_id,
|
||||
'product_name' => $item->product?->name,
|
||||
'quantity' => (float) $item->quantity,
|
||||
'unit_id' => $item->unit_id,
|
||||
'unit_name' => $item->unit?->name,
|
||||
'subtotal' => (float) $item->subtotal,
|
||||
];
|
||||
})->keyBy('product_id');
|
||||
|
||||
// Find removed
|
||||
foreach ($oldItems as $productId => $oldItem) {
|
||||
if (!$newItemsFormatted->has($productId)) {
|
||||
$itemDiffs['removed'][] = $oldItem;
|
||||
}
|
||||
}
|
||||
|
||||
// Find added and updated
|
||||
foreach ($newItemsFormatted as $productId => $newItem) {
|
||||
if (!$oldItems->has($productId)) {
|
||||
$itemDiffs['added'][] = $newItem;
|
||||
} else {
|
||||
$oldItem = $oldItems[$productId];
|
||||
// Compare fields
|
||||
if (
|
||||
$oldItem['quantity'] != $newItem['quantity'] ||
|
||||
$oldItem['unit_id'] != $newItem['unit_id'] ||
|
||||
$oldItem['subtotal'] != $newItem['subtotal']
|
||||
) {
|
||||
$itemDiffs['updated'][] = [
|
||||
'product_name' => $newItem['product_name'],
|
||||
'old' => [
|
||||
'quantity' => $oldItem['quantity'],
|
||||
'unit_name' => $oldItem['unit_name'],
|
||||
'subtotal' => $oldItem['subtotal'],
|
||||
],
|
||||
'new' => [
|
||||
'quantity' => $newItem['quantity'],
|
||||
'unit_name' => $newItem['unit_name'],
|
||||
'subtotal' => $newItem['subtotal'],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Manually Log activity (Single Consolidated Log)
|
||||
// Log if there are attribute changes OR item changes
|
||||
if (!empty($newAttributes) || !empty($itemDiffs['added']) || !empty($itemDiffs['removed']) || !empty($itemDiffs['updated'])) {
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('updated')
|
||||
->withProperties([
|
||||
'attributes' => $newAttributes,
|
||||
'old' => $oldAttributes,
|
||||
'items_diff' => $itemDiffs,
|
||||
'snapshot' => [
|
||||
'po_number' => $order->code,
|
||||
'vendor_name' => $order->vendor?->name,
|
||||
'warehouse_name' => $order->warehouse?->name,
|
||||
'user_name' => $order->user?->name,
|
||||
]
|
||||
])
|
||||
->log('updated');
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
@@ -383,9 +494,43 @@ class PurchaseOrderController extends Controller
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$order = PurchaseOrder::findOrFail($id);
|
||||
$order = PurchaseOrder::with(['items.product', 'items.unit'])->findOrFail($id);
|
||||
|
||||
// Delete associated items first (due to FK constraints if not cascade)
|
||||
// Capture items for logging
|
||||
$items = $order->items->map(function ($item) {
|
||||
return [
|
||||
'product_name' => $item->product_name,
|
||||
'quantity' => floatval($item->quantity),
|
||||
'unit_name' => $item->unit_name,
|
||||
'subtotal' => floatval($item->subtotal),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
// Manually log the deletion with items
|
||||
activity()
|
||||
->performedOn($order)
|
||||
->causedBy(auth()->user())
|
||||
->event('deleted')
|
||||
->withProperties([
|
||||
'attributes' => $order->getAttributes(),
|
||||
'items_diff' => [
|
||||
'added' => [],
|
||||
'removed' => $items,
|
||||
'updated' => [],
|
||||
],
|
||||
'snapshot' => [
|
||||
'po_number' => $order->code,
|
||||
'vendor_name' => $order->vendor?->name,
|
||||
'warehouse_name' => $order->warehouse?->name,
|
||||
'user_name' => $order->user?->name,
|
||||
]
|
||||
])
|
||||
->log('deleted');
|
||||
|
||||
// Disable automatic logging for this operation
|
||||
$order->disableLogging();
|
||||
|
||||
// Delete associated items first
|
||||
$order->items()->delete();
|
||||
$order->delete();
|
||||
|
||||
|
||||
@@ -27,6 +27,25 @@ class VendorProductController extends Controller
|
||||
'last_price' => $validated['last_price'] ?? null
|
||||
]);
|
||||
|
||||
// 記錄操作
|
||||
$product = \App\Models\Product::find($validated['product_id']);
|
||||
activity()
|
||||
->performedOn($vendor)
|
||||
->withProperties([
|
||||
'attributes' => [
|
||||
'product_name' => $product->name,
|
||||
'last_price' => $validated['last_price'] ?? null,
|
||||
],
|
||||
'sub_subject' => '供貨商品',
|
||||
'snapshot' => [
|
||||
'name' => "{$vendor->name}-{$product->name}", // 顯示例如:台積電-紅糖
|
||||
'vendor_name' => $vendor->name,
|
||||
'product_name' => $product->name,
|
||||
]
|
||||
])
|
||||
->event('created')
|
||||
->log('新增供貨商品');
|
||||
|
||||
return redirect()->back()->with('success', '供貨商品已新增');
|
||||
}
|
||||
|
||||
@@ -39,10 +58,34 @@ class VendorProductController extends Controller
|
||||
'last_price' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
// 獲取舊價格
|
||||
$old_price = $vendor->products()->where('product_id', $productId)->first()?->pivot?->last_price;
|
||||
|
||||
$vendor->products()->updateExistingPivot($productId, [
|
||||
'last_price' => $validated['last_price'] ?? null
|
||||
]);
|
||||
|
||||
// 記錄操作
|
||||
$product = \App\Models\Product::find($productId);
|
||||
activity()
|
||||
->performedOn($vendor)
|
||||
->withProperties([
|
||||
'old' => [
|
||||
'last_price' => $old_price,
|
||||
],
|
||||
'attributes' => [
|
||||
'last_price' => $validated['last_price'] ?? null,
|
||||
],
|
||||
'sub_subject' => '供貨商品',
|
||||
'snapshot' => [
|
||||
'name' => "{$vendor->name}-{$product->name}",
|
||||
'vendor_name' => $vendor->name,
|
||||
'product_name' => $product->name,
|
||||
]
|
||||
])
|
||||
->event('updated')
|
||||
->log('更新供貨商品價格');
|
||||
|
||||
return redirect()->back()->with('success', '供貨資訊已更新');
|
||||
}
|
||||
|
||||
@@ -51,8 +94,31 @@ class VendorProductController extends Controller
|
||||
*/
|
||||
public function destroy(Vendor $vendor, $productId)
|
||||
{
|
||||
// 記錄操作 (需在 detach 前獲取資訊)
|
||||
$product = \App\Models\Product::find($productId);
|
||||
$old_price = $vendor->products()->where('product_id', $productId)->first()?->pivot?->last_price;
|
||||
|
||||
$vendor->products()->detach($productId);
|
||||
|
||||
if ($product) {
|
||||
activity()
|
||||
->performedOn($vendor)
|
||||
->withProperties([
|
||||
'old' => [
|
||||
'product_name' => $product->name,
|
||||
'last_price' => $old_price,
|
||||
],
|
||||
'sub_subject' => '供貨商品',
|
||||
'snapshot' => [
|
||||
'name' => "{$vendor->name}-{$product->name}",
|
||||
'vendor_name' => $vendor->name,
|
||||
'product_name' => $product->name,
|
||||
]
|
||||
])
|
||||
->event('deleted')
|
||||
->log('移除供貨商品');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', '供貨商品已移除');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,11 @@ class Category extends Model
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
$attributes['name'] = $this->name;
|
||||
$properties['attributes'] = $attributes;
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
$snapshot['name'] = $this->name;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,12 @@ class Inventory extends Model
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
|
||||
// Always snapshot names for context, even if IDs didn't change
|
||||
// $this refers to the Inventory model instance
|
||||
$attributes['warehouse_name'] = $this->warehouse ? $this->warehouse->name : ($attributes['warehouse_name'] ?? null);
|
||||
$attributes['product_name'] = $this->product ? $this->product->name : ($attributes['product_name'] ?? null);
|
||||
$snapshot['warehouse_name'] = $this->warehouse ? $this->warehouse->name : ($snapshot['warehouse_name'] ?? null);
|
||||
$snapshot['product_name'] = $this->product ? $this->product->name : ($snapshot['product_name'] ?? null);
|
||||
|
||||
// Capture the reason if set
|
||||
if ($this->activityLogReason) {
|
||||
@@ -50,6 +51,7 @@ class Inventory extends Model
|
||||
}
|
||||
|
||||
$properties['attributes'] = $attributes;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,11 +80,12 @@ class Product extends Model
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
|
||||
// Handle Category Name Snapshot
|
||||
if (isset($attributes['category_id'])) {
|
||||
$category = Category::find($attributes['category_id']);
|
||||
$attributes['category_name'] = $category ? $category->name : null;
|
||||
$snapshot['category_name'] = $category ? $category->name : null;
|
||||
}
|
||||
|
||||
// Handle Unit Name Snapshots
|
||||
@@ -93,14 +94,15 @@ class Product extends Model
|
||||
if (isset($attributes[$field])) {
|
||||
$unit = Unit::find($attributes[$field]);
|
||||
$nameKey = str_replace('_id', '_name', $field);
|
||||
$attributes[$nameKey] = $unit ? $unit->name : null;
|
||||
$snapshot[$nameKey] = $unit ? $unit->name : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Always snapshot self name for context (so logs always show "Cola")
|
||||
$attributes['name'] = $this->name;
|
||||
$snapshot['name'] = $this->name;
|
||||
|
||||
$properties['attributes'] = $attributes;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ class PurchaseOrder extends Model
|
||||
'supplierName',
|
||||
'expectedDate',
|
||||
'totalAmount',
|
||||
'taxAmount', // Add this
|
||||
'grandTotal', // Add this
|
||||
'createdBy',
|
||||
'warehouse_name',
|
||||
'createdAt',
|
||||
@@ -82,6 +84,16 @@ class PurchaseOrder extends Model
|
||||
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 : '系統';
|
||||
@@ -135,4 +147,21 @@ class PurchaseOrder extends Model
|
||||
->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ class Unit extends Model
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
$attributes['name'] = $this->name;
|
||||
$properties['attributes'] = $attributes;
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
$snapshot['name'] = $this->name;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,19 @@ class Vendor extends Model
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,11 @@ class Warehouse extends Model
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
$properties = $activity->properties;
|
||||
$attributes = $properties['attributes'] ?? [];
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
$snapshot['name'] = $this->name;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
// Always snapshot name
|
||||
$attributes['name'] = $this->name;
|
||||
|
||||
$properties['attributes'] = $attributes;
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user