feat(inventory): 重構庫存盤點流程與優化操作日誌
1. 重構盤點流程:實作自動狀態轉換(盤點中/盤點完成)、整合按鈕為「儲存盤點結果」、更名 UI 狀態標籤。 2. 優化操作日誌: - 實作全域 ID 轉名稱邏輯(倉庫、使用者)。 - 合併單次操作的日誌記錄,避免重複產生。 - 修復日誌產生過程中的 Collection 修改錯誤。 3. 修正 TypeScript lint 錯誤(Index, Show 頁面)。
This commit is contained in:
@@ -28,6 +28,7 @@ class ActivityLogController extends Controller
|
|||||||
'App\Modules\Production\Models\Recipe' => '生產配方',
|
'App\Modules\Production\Models\Recipe' => '生產配方',
|
||||||
'App\Modules\Production\Models\RecipeItem' => '配方品項',
|
'App\Modules\Production\Models\RecipeItem' => '配方品項',
|
||||||
'App\Modules\Production\Models\ProductionOrderItem' => '工單品項',
|
'App\Modules\Production\Models\ProductionOrderItem' => '工單品項',
|
||||||
|
'App\Modules\Inventory\Models\InventoryCountDoc' => '庫存盤點單',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class CountDocController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 自動執行快照
|
// 自動執行快照
|
||||||
$this->countService->snapshot($doc);
|
$this->countService->snapshot($doc, false);
|
||||||
|
|
||||||
return redirect()->route('inventory.count.show', [$doc->id])
|
return redirect()->route('inventory.count.show', [$doc->id])
|
||||||
->with('success', '已建立盤點單並完成庫存快照');
|
->with('success', '已建立盤點單並完成庫存快照');
|
||||||
@@ -173,14 +173,37 @@ class CountDocController extends Controller
|
|||||||
$this->countService->updateCount($doc, $validated['items']);
|
$this->countService->updateCount($doc, $validated['items']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是按了 "完成盤點"
|
// 重新讀取以獲取最新狀態
|
||||||
if ($request->input('action') === 'complete') {
|
$doc->refresh();
|
||||||
$this->countService->complete($doc, auth()->id());
|
|
||||||
|
if ($doc->status === 'completed') {
|
||||||
return redirect()->route('inventory.count.index')
|
return redirect()->route('inventory.count.index')
|
||||||
->with('success', '盤點單已完成');
|
->with('success', '盤點完成,單據已自動存檔並完成。');
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()->with('success', '暫存成功');
|
return redirect()->back()->with('success', '盤點資料已暫存');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reopen(InventoryCountDoc $doc)
|
||||||
|
{
|
||||||
|
// 權限檢查 (通常僅允許有權限者執行,例如 inventory.adjust)
|
||||||
|
// 注意:前端已經用 <Can> 保護按鈕,後端這裡最好也加上檢查
|
||||||
|
if (!auth()->user()->can('inventory.adjust')) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($doc->status !== 'completed') {
|
||||||
|
return redirect()->back()->with('error', '僅能針對已完成的盤點單重新開啟盤點');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行取消核准邏輯
|
||||||
|
$doc->update([
|
||||||
|
'status' => 'counting', // 回復為盤點中
|
||||||
|
'completed_at' => null, // 清除完成時間
|
||||||
|
'completed_by' => null, // 清除完成者
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '已重新開啟盤點,單據回復為盤點中狀態');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(InventoryCountDoc $doc)
|
public function destroy(InventoryCountDoc $doc)
|
||||||
@@ -189,18 +212,7 @@ class CountDocController extends Controller
|
|||||||
return redirect()->back()->with('error', '已完成的盤點單無法刪除');
|
return redirect()->back()->with('error', '已完成的盤點單無法刪除');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 記錄活動
|
// Activity Log handled by Model Trait
|
||||||
activity()
|
|
||||||
->performedOn($doc)
|
|
||||||
->causedBy(auth()->user())
|
|
||||||
->event('deleted')
|
|
||||||
->withProperties([
|
|
||||||
'snapshot' => [
|
|
||||||
'doc_no' => $doc->doc_no,
|
|
||||||
'warehouse_name' => $doc->warehouse?->name,
|
|
||||||
]
|
|
||||||
])
|
|
||||||
->log('deleted');
|
|
||||||
|
|
||||||
$doc->items()->delete();
|
$doc->items()->delete();
|
||||||
$doc->delete();
|
$doc->delete();
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use App\Modules\Core\Models\User;
|
use App\Modules\Core\Models\User;
|
||||||
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
|
||||||
class InventoryCountDoc extends Model
|
class InventoryCountDoc extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use LogsActivity;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'doc_no',
|
'doc_no',
|
||||||
@@ -75,4 +78,65 @@ class InventoryCountDoc extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'completed_by');
|
return $this->belongsTo(User::class, 'completed_by');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
||||||
|
{
|
||||||
|
return \Spatie\Activitylog\LogOptions::defaults()
|
||||||
|
->logAll()
|
||||||
|
->logOnlyDirty()
|
||||||
|
->dontSubmitEmptyLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||||
|
{
|
||||||
|
// 確保為陣列以進行修改
|
||||||
|
$properties = $activity->properties instanceof \Illuminate\Support\Collection
|
||||||
|
? $activity->properties->toArray()
|
||||||
|
: $activity->properties;
|
||||||
|
|
||||||
|
$snapshot = $properties['snapshot'] ?? [];
|
||||||
|
|
||||||
|
// Snapshot key information
|
||||||
|
$snapshot['doc_no'] = $this->doc_no;
|
||||||
|
$snapshot['warehouse_name'] = $this->warehouse ? $this->warehouse->name : null;
|
||||||
|
$snapshot['completed_at'] = $this->completed_at ? $this->completed_at->format('Y-m-d H:i:s') : null;
|
||||||
|
$snapshot['status'] = $this->status;
|
||||||
|
$snapshot['created_by_name'] = $this->createdBy ? $this->createdBy->name : null;
|
||||||
|
$snapshot['completed_by_name'] = $this->completedBy ? $this->completedBy->name : null;
|
||||||
|
|
||||||
|
$properties['snapshot'] = $snapshot;
|
||||||
|
|
||||||
|
// 全域 ID 轉名稱邏輯 (用於 attributes 與 old)
|
||||||
|
$convertIdsToNames = function (&$data) {
|
||||||
|
if (empty($data) || !is_array($data)) return;
|
||||||
|
|
||||||
|
// 倉庫 ID 轉換
|
||||||
|
if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) {
|
||||||
|
$warehouse = \App\Modules\Inventory\Models\Warehouse::find($data['warehouse_id']);
|
||||||
|
if ($warehouse) {
|
||||||
|
$data['warehouse_id'] = $warehouse->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用者 ID 轉換
|
||||||
|
$userFields = ['created_by', 'updated_by', 'completed_by'];
|
||||||
|
foreach ($userFields as $field) {
|
||||||
|
if (isset($data[$field]) && is_numeric($data[$field])) {
|
||||||
|
$user = \App\Modules\Core\Models\User::find($data[$field]);
|
||||||
|
if ($user) {
|
||||||
|
$data[$field] = $user->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isset($properties['attributes'])) {
|
||||||
|
$convertIdsToNames($properties['attributes']);
|
||||||
|
}
|
||||||
|
if (isset($properties['old'])) {
|
||||||
|
$convertIdsToNames($properties['old']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activity->properties = $properties;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class CountService
|
|||||||
return DB::transaction(function () use ($warehouseId, $remarks, $userId) {
|
return DB::transaction(function () use ($warehouseId, $remarks, $userId) {
|
||||||
$doc = InventoryCountDoc::create([
|
$doc = InventoryCountDoc::create([
|
||||||
'warehouse_id' => $warehouseId,
|
'warehouse_id' => $warehouseId,
|
||||||
'status' => 'draft',
|
'status' => 'counting',
|
||||||
|
'snapshot_date' => now(),
|
||||||
'remarks' => $remarks,
|
'remarks' => $remarks,
|
||||||
'created_by' => $userId,
|
'created_by' => $userId,
|
||||||
]);
|
]);
|
||||||
@@ -32,9 +33,9 @@ class CountService
|
|||||||
/**
|
/**
|
||||||
* 執行快照:鎖定當前庫存量
|
* 執行快照:鎖定當前庫存量
|
||||||
*/
|
*/
|
||||||
public function snapshot(InventoryCountDoc $doc): void
|
public function snapshot(InventoryCountDoc $doc, bool $updateDoc = true): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($doc) {
|
DB::transaction(function () use ($doc, $updateDoc) {
|
||||||
// 清除舊的 items (如果有)
|
// 清除舊的 items (如果有)
|
||||||
$doc->items()->delete();
|
$doc->items()->delete();
|
||||||
|
|
||||||
@@ -62,10 +63,12 @@ class CountService
|
|||||||
InventoryCountItem::insert($items);
|
InventoryCountItem::insert($items);
|
||||||
}
|
}
|
||||||
|
|
||||||
$doc->update([
|
if ($updateDoc) {
|
||||||
'status' => 'counting',
|
$doc->update([
|
||||||
'snapshot_date' => now(),
|
'status' => 'counting',
|
||||||
]);
|
'snapshot_date' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,19 +94,111 @@ class CountService
|
|||||||
public function updateCount(InventoryCountDoc $doc, array $itemsData): void
|
public function updateCount(InventoryCountDoc $doc, array $itemsData): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($doc, $itemsData) {
|
DB::transaction(function () use ($doc, $itemsData) {
|
||||||
foreach ($itemsData as $data) {
|
$updatedItems = [];
|
||||||
$item = $doc->items()->find($data['id']);
|
$hasChanges = false;
|
||||||
if ($item) {
|
$oldDocAttributes = [
|
||||||
$countedQty = $data['counted_qty'];
|
'status' => $doc->status,
|
||||||
$diff = is_numeric($countedQty) ? ($countedQty - $item->system_qty) : 0;
|
'completed_at' => $doc->completed_at ? $doc->completed_at->format('Y-m-d H:i:s') : null,
|
||||||
|
'completed_by' => $doc->completed_by,
|
||||||
|
];
|
||||||
|
|
||||||
$item->update([
|
foreach ($itemsData as $data) {
|
||||||
'counted_qty' => $countedQty,
|
$item = $doc->items()->with('product')->find($data['id']);
|
||||||
'diff_qty' => $diff,
|
if ($item) {
|
||||||
'notes' => $data['notes'] ?? $item->notes,
|
$oldQty = $item->counted_qty;
|
||||||
]);
|
$newQty = $data['counted_qty'];
|
||||||
|
$oldNotes = $item->notes;
|
||||||
|
$newNotes = $data['notes'] ?? $item->notes;
|
||||||
|
|
||||||
|
$isQtyChanged = $oldQty != $newQty;
|
||||||
|
$isNotesChanged = $oldNotes !== $newNotes;
|
||||||
|
|
||||||
|
if ($isQtyChanged || $isNotesChanged) {
|
||||||
|
$updatedItems[] = [
|
||||||
|
'product_name' => $item->product->name,
|
||||||
|
'old' => [
|
||||||
|
'counted_qty' => $oldQty,
|
||||||
|
'notes' => $oldNotes,
|
||||||
|
],
|
||||||
|
'new' => [
|
||||||
|
'counted_qty' => $newQty,
|
||||||
|
'notes' => $newNotes,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$countedQty = $data['counted_qty'];
|
||||||
|
$diff = is_numeric($countedQty) ? ($countedQty - $item->system_qty) : 0;
|
||||||
|
|
||||||
|
$item->update([
|
||||||
|
'counted_qty' => $countedQty,
|
||||||
|
'diff_qty' => $diff,
|
||||||
|
'notes' => $newNotes,
|
||||||
|
]);
|
||||||
|
$hasChanges = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 檢查是否完成
|
||||||
|
$doc->refresh();
|
||||||
|
$isAllCounted = $doc->items()->whereNull('counted_qty')->count() === 0;
|
||||||
|
$newDocAttributesLog = [];
|
||||||
|
|
||||||
|
if ($isAllCounted) {
|
||||||
|
if ($doc->status !== 'completed') {
|
||||||
|
$doc->status = 'completed';
|
||||||
|
$doc->completed_at = now();
|
||||||
|
$doc->completed_by = auth()->id();
|
||||||
|
$doc->saveQuietly();
|
||||||
|
|
||||||
|
$doc->refresh(); // 獲取更新後的屬性 (如時間)
|
||||||
|
|
||||||
|
$newDocAttributesLog = [
|
||||||
|
'status' => 'completed',
|
||||||
|
'completed_at' => $doc->completed_at->format('Y-m-d H:i:s'),
|
||||||
|
'completed_by' => $doc->completed_by,
|
||||||
|
];
|
||||||
|
$hasChanges = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($doc->status === 'completed') {
|
||||||
|
$doc->status = 'counting';
|
||||||
|
$doc->completed_at = null;
|
||||||
|
$doc->completed_by = null;
|
||||||
|
$doc->saveQuietly();
|
||||||
|
|
||||||
|
$newDocAttributesLog = [
|
||||||
|
'status' => 'counting',
|
||||||
|
'completed_at' => null,
|
||||||
|
'completed_by' => null,
|
||||||
|
];
|
||||||
|
$hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 記錄操作日誌
|
||||||
|
if ($hasChanges) {
|
||||||
|
$properties = [
|
||||||
|
'items_diff' => [
|
||||||
|
'added' => [],
|
||||||
|
'removed' => [],
|
||||||
|
'updated' => $updatedItems,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果有文件層級的屬性變更 (狀態),併入 log
|
||||||
|
if (!empty($newDocAttributesLog)) {
|
||||||
|
$properties['attributes'] = $newDocAttributesLog;
|
||||||
|
$properties['old'] = array_intersect_key($oldDocAttributes, $newDocAttributesLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
activity()
|
||||||
|
->performedOn($doc)
|
||||||
|
->causedBy(auth()->user())
|
||||||
|
->event('updated')
|
||||||
|
->withProperties($properties)
|
||||||
|
->log('updated');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,10 @@ const fieldLabels: Record<string, string> = {
|
|||||||
reason: '原因',
|
reason: '原因',
|
||||||
count_doc_id: '盤點單 ID',
|
count_doc_id: '盤點單 ID',
|
||||||
count_doc_no: '盤點單號',
|
count_doc_no: '盤點單號',
|
||||||
|
created_by: '建立者',
|
||||||
|
updated_by: '更新者',
|
||||||
|
completed_by: '完成者',
|
||||||
|
counted_qty: '盤點數量',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 狀態翻譯對照表
|
// 狀態翻譯對照表
|
||||||
@@ -271,6 +275,25 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
|||||||
return value.split('T')[0].split(' ')[0];
|
return value.split('T')[0].split(' ')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 處理日期時間欄位 (YYYY-MM-DD HH:mm:ss)
|
||||||
|
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at') && typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const date = new Date(value);
|
||||||
|
return date.toLocaleString('zh-TW', {
|
||||||
|
timeZone: 'Asia/Taipei',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
} catch (e) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +324,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
|||||||
return `${wName} - ${pName}`;
|
return `${wName} - ${pName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameParams = ['po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
|
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
|
||||||
for (const param of nameParams) {
|
for (const param of nameParams) {
|
||||||
if (snapshot[param]) return snapshot[param];
|
if (snapshot[param]) return snapshot[param];
|
||||||
if (attributes[param]) return attributes[param];
|
if (attributes[param]) return attributes[param];
|
||||||
@@ -480,12 +503,18 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
|||||||
{item.old.quantity !== item.new.quantity && (
|
{item.old.quantity !== item.new.quantity && (
|
||||||
<div>數量: <span className="text-gray-500 line-through">{item.old.quantity}</span> → <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
|
<div>數量: <span className="text-gray-500 line-through">{item.old.quantity}</span> → <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
|
||||||
)}
|
)}
|
||||||
|
{item.old.counted_qty !== item.new.counted_qty && (
|
||||||
|
<div>盤點量: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> → <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
|
||||||
|
)}
|
||||||
{item.old.unit_name !== item.new.unit_name && (
|
{item.old.unit_name !== item.new.unit_name && (
|
||||||
<div>單位: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> → <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
|
<div>單位: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> → <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
|
||||||
)}
|
)}
|
||||||
{item.old.subtotal !== item.new.subtotal && (
|
{item.old.subtotal !== item.new.subtotal && (
|
||||||
<div>小計: <span className="text-gray-500 line-through">${item.old.subtotal}</span> → <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
|
<div>小計: <span className="text-gray-500 line-through">${item.old.subtotal}</span> → <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
|
||||||
)}
|
)}
|
||||||
|
{item.old.notes !== item.new.notes && (
|
||||||
|
<div>備註: <span className="text-gray-500 line-through">{item.old.notes || '-'}</span> → <span className="text-blue-700 font-bold">{item.new.notes || '-'}</span></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
39
resources/js/Components/ActivityLog/ActivityLog.tsx
Normal file
39
resources/js/Components/ActivityLog/ActivityLog.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import LogTable, { Activity } from './LogTable';
|
||||||
|
import ActivityDetailDialog from './ActivityDetailDialog';
|
||||||
|
import { History } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activities: Activity[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ActivityLog({ activities, className = '' }: Props) {
|
||||||
|
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
|
||||||
|
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleViewDetail = (activity: Activity) => {
|
||||||
|
setSelectedActivity(activity);
|
||||||
|
setIsDetailOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-4 ${className}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<History className="h-5 w-5 text-gray-500" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">操作紀錄</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LogTable
|
||||||
|
activities={activities}
|
||||||
|
onViewDetail={handleViewDetail}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActivityDetailDialog
|
||||||
|
open={isDetailOpen}
|
||||||
|
onOpenChange={setIsDetailOpen}
|
||||||
|
activity={selectedActivity}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export default function LogTable({
|
|||||||
|
|
||||||
// 嘗試在快照、屬性或舊值中尋找名稱
|
// 嘗試在快照、屬性或舊值中尋找名稱
|
||||||
// 優先順序:快照 > 特定名稱欄位 > 通用名稱 > 代碼 > ID
|
// 優先順序:快照 > 特定名稱欄位 > 通用名稱 > 代碼 > ID
|
||||||
const nameParams = ['po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title', 'category'];
|
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title', 'category'];
|
||||||
let subjectName = '';
|
let subjectName = '';
|
||||||
|
|
||||||
// 庫存的特殊處理:顯示 "倉庫 - 商品"
|
// 庫存的特殊處理:顯示 "倉庫 - 商品"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||||
import { Head, Link, useForm, router, usePage } from '@inertiajs/react';
|
import { Head, Link, useForm, router } from '@inertiajs/react';
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { usePermission } from '@/hooks/usePermission';
|
import { usePermission } from '@/hooks/usePermission';
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
import Pagination from '@/Components/shared/Pagination';
|
import Pagination from '@/Components/shared/Pagination';
|
||||||
import { Can } from '@/Components/Permission/Can';
|
import { Can } from '@/Components/Permission/Can';
|
||||||
|
|
||||||
export default function Index({ auth, docs, warehouses, filters }: any) {
|
export default function Index({ docs, warehouses, filters }: any) {
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
const { data, setData, post, processing, reset, errors, delete: destroy } = useForm({
|
const { data, setData, post, processing, reset, errors, delete: destroy } = useForm({
|
||||||
@@ -112,7 +112,7 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = (e) => {
|
const handleCreate = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
post(route('inventory.count.store'), {
|
post(route('inventory.count.store'), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -135,14 +135,14 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status) => {
|
const getStatusBadge = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'draft':
|
case 'draft':
|
||||||
return <Badge variant="secondary">草稿</Badge>;
|
return <Badge variant="secondary">草稿</Badge>;
|
||||||
case 'counting':
|
case 'counting':
|
||||||
return <Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>;
|
return <Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return <Badge className="bg-green-500 hover:bg-green-600">已核准</Badge>;
|
return <Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>;
|
||||||
case 'adjusted':
|
case 'adjusted':
|
||||||
return <Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>;
|
return <Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>;
|
||||||
case 'cancelled':
|
case 'cancelled':
|
||||||
@@ -287,7 +287,7 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
docs.data.map((doc, index) => (
|
docs.data.map((doc: any, index: number) => (
|
||||||
<TableRow key={doc.id}>
|
<TableRow key={doc.id}>
|
||||||
<TableCell className="text-gray-500 font-medium text-center">
|
<TableCell className="text-gray-500 font-medium text-center">
|
||||||
{(docs.current_page - 1) * docs.per_page + index + 1}
|
{(docs.current_page - 1) * docs.per_page + index + 1}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { Button } from '@/Components/ui/button';
|
import { Button } from '@/Components/ui/button';
|
||||||
import { Input } from '@/Components/ui/input';
|
import { Input } from '@/Components/ui/input';
|
||||||
import { Badge } from '@/Components/ui/badge';
|
import { Badge } from '@/Components/ui/badge';
|
||||||
import { Save, CheckCircle, Printer, Trash2, ClipboardCheck, ArrowLeft, RotateCcw } from 'lucide-react'; // Added ArrowLeft
|
import { Save, Printer, Trash2, ClipboardCheck, ArrowLeft, RotateCcw } from 'lucide-react'; // Added ArrowLeft
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from "@/Components/ui/alert-dialog"
|
} from "@/Components/ui/alert-dialog"
|
||||||
import { Can } from '@/Components/Permission/Can';
|
import { Can } from '@/Components/Permission/Can';
|
||||||
|
|
||||||
|
|
||||||
export default function Show({ doc }: any) {
|
export default function Show({ doc }: any) {
|
||||||
// Transform items to form data structure
|
// Transform items to form data structure
|
||||||
const { data, setData, put, delete: destroy, processing, transform } = useForm({
|
const { data, setData, put, delete: destroy, processing, transform } = useForm({
|
||||||
@@ -102,7 +103,7 @@ export default function Show({ doc }: any) {
|
|||||||
盤點單: {doc.doc_no}
|
盤點單: {doc.doc_no}
|
||||||
</h1>
|
</h1>
|
||||||
{doc.status === 'completed' && (
|
{doc.status === 'completed' && (
|
||||||
<Badge className="bg-green-500 hover:bg-green-600">已核准</Badge>
|
<Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>
|
||||||
)}
|
)}
|
||||||
{doc.status === 'adjusted' && (
|
{doc.status === 'adjusted' && (
|
||||||
<Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>
|
<Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>
|
||||||
@@ -138,19 +139,19 @@ export default function Show({ doc }: any) {
|
|||||||
disabled={processing}
|
disabled={processing}
|
||||||
>
|
>
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
取消核准
|
重新開啟盤點
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>確定要取消核准嗎?</AlertDialogTitle>
|
<AlertDialogTitle>確定要重新開啟盤點嗎?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
單據將回復為「盤點中」狀態,若已產生庫存異動將被撤回。此動作可讓您重新編輯盤點數量。
|
單據將回復為「盤點中」狀態。此動作可讓您重新編輯盤點數量。
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleReopen} className="bg-red-600 hover:bg-red-700">確認取消核准</AlertDialogAction>
|
<AlertDialogAction onClick={handleReopen} className="bg-red-600 hover:bg-red-700">確認重新開啟</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
@@ -184,23 +185,13 @@ export default function Show({ doc }: any) {
|
|||||||
|
|
||||||
<Can permission="inventory_count.edit">
|
<Can permission="inventory_count.edit">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
className="button-outlined-primary"
|
className="button-filled-primary"
|
||||||
onClick={() => handleSubmit('save')}
|
onClick={() => handleSubmit('save')}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
更新
|
儲存盤點結果
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="button-filled-primary"
|
|
||||||
onClick={() => handleSubmit('complete')}
|
|
||||||
disabled={processing}
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
完成
|
|
||||||
</Button>
|
</Button>
|
||||||
</Can>
|
</Can>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,6 +309,7 @@ export default function Show({ doc }: any) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</AuthenticatedLayout >
|
</AuthenticatedLayout >
|
||||||
|
|||||||
Reference in New Issue
Block a user