feat(inventory): 重構庫存盤點流程與優化操作日誌
1. 重構盤點流程:實作自動狀態轉換(盤點中/盤點完成)、整合按鈕為「儲存盤點結果」、更名 UI 狀態標籤。 2. 優化操作日誌: - 實作全域 ID 轉名稱邏輯(倉庫、使用者)。 - 合併單次操作的日誌記錄,避免重複產生。 - 修復日誌產生過程中的 Collection 修改錯誤。 3. 修正 TypeScript lint 錯誤(Index, Show 頁面)。
This commit is contained in:
@@ -143,6 +143,10 @@ const fieldLabels: Record<string, string> = {
|
||||
reason: '原因',
|
||||
count_doc_id: '盤點單 ID',
|
||||
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];
|
||||
}
|
||||
|
||||
// 處理日期時間欄位 (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);
|
||||
};
|
||||
|
||||
@@ -301,7 +324,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
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) {
|
||||
if (snapshot[param]) return snapshot[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 && (
|
||||
<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 && (
|
||||
<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 && (
|
||||
<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>
|
||||
</TableCell>
|
||||
</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
|
||||
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 = '';
|
||||
|
||||
// 庫存的特殊處理:顯示 "倉庫 - 商品"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { usePermission } from '@/hooks/usePermission';
|
||||
import { debounce } from "lodash";
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
import Pagination from '@/Components/shared/Pagination';
|
||||
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 [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
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();
|
||||
post(route('inventory.count.store'), {
|
||||
onSuccess: () => {
|
||||
@@ -135,14 +135,14 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return <Badge variant="secondary">草稿</Badge>;
|
||||
case 'counting':
|
||||
return <Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>;
|
||||
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':
|
||||
return <Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>;
|
||||
case 'cancelled':
|
||||
@@ -287,7 +287,7 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
docs.data.map((doc, index) => (
|
||||
docs.data.map((doc: any, index: number) => (
|
||||
<TableRow key={doc.id}>
|
||||
<TableCell className="text-gray-500 font-medium text-center">
|
||||
{(docs.current_page - 1) * docs.per_page + index + 1}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Input } from '@/Components/ui/input';
|
||||
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 {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@/Components/ui/alert-dialog"
|
||||
import { Can } from '@/Components/Permission/Can';
|
||||
|
||||
|
||||
export default function Show({ doc }: any) {
|
||||
// Transform items to form data structure
|
||||
const { data, setData, put, delete: destroy, processing, transform } = useForm({
|
||||
@@ -102,7 +103,7 @@ export default function Show({ doc }: any) {
|
||||
盤點單: {doc.doc_no}
|
||||
</h1>
|
||||
{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' && (
|
||||
<Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>
|
||||
@@ -138,19 +139,19 @@ export default function Show({ doc }: any) {
|
||||
disabled={processing}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
取消核准
|
||||
重新開啟盤點
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要取消核准嗎?</AlertDialogTitle>
|
||||
<AlertDialogTitle>確定要重新開啟盤點嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
單據將回復為「盤點中」狀態,若已產生庫存異動將被撤回。此動作可讓您重新編輯盤點數量。
|
||||
單據將回復為「盤點中」狀態。此動作可讓您重新編輯盤點數量。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<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>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@@ -184,23 +185,13 @@ export default function Show({ doc }: any) {
|
||||
|
||||
<Can permission="inventory_count.edit">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
className="button-filled-primary"
|
||||
onClick={() => handleSubmit('save')}
|
||||
disabled={processing}
|
||||
>
|
||||
<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>
|
||||
</Can>
|
||||
</div>
|
||||
@@ -318,6 +309,7 @@ export default function Show({ doc }: any) {
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</AuthenticatedLayout >
|
||||
|
||||
Reference in New Issue
Block a user