feat: 優化採購單操作紀錄與統一刪除確認 UI

- 優化採購單更新與刪除的活動紀錄邏輯 (PurchaseOrderController)
  - 整合更新異動為單一紀錄,包含品項差異
  - 刪除時記錄當下品項快照
- 統一採購單刪除確認介面,使用 AlertDialog 取代原生 confirm (PurchaseOrderActions)
- Refactor: 將 ActivityDetailDialog 移至 Components/ActivityLog 並優化樣式與大數據顯示
- 調整 UI 文字:將「總金額」統一為「小計」
- 其他模型與 Controller 的活動紀錄支援更新
This commit is contained in:
2026-01-19 15:32:41 +08:00
parent 18edb3cb69
commit a8091276b8
20 changed files with 1114 additions and 482 deletions

View File

@@ -3,6 +3,7 @@
*/
import { ArrowLeft, Plus, Info, ShoppingCart } from "lucide-react";
import { useEffect } from "react";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea";
@@ -58,11 +59,23 @@ export default function CreatePurchaseOrder({
setInvoiceNumber,
setInvoiceDate,
setInvoiceAmount,
taxAmount,
setTaxAmount,
isTaxManual,
setIsTaxManual,
} = usePurchaseOrderForm({ order, suppliers });
const totalAmount = calculateTotalAmount(items);
// Auto-calculate tax if not manual
useEffect(() => {
if (!isTaxManual) {
const calculatedTax = Math.round(totalAmount * 0.05);
setTaxAmount(calculatedTax);
}
}, [totalAmount, isTaxManual]);
const handleSave = () => {
if (!warehouseId) {
toast.error("請選擇入庫倉庫");
@@ -113,6 +126,7 @@ export default function CreatePurchaseOrder({
invoice_number: invoiceNumber || null,
invoice_date: invoiceDate || null,
invoice_amount: invoiceAmount ? parseFloat(invoiceAmount) : null,
tax_amount: Number(taxAmount) || 0,
items: validItems.map(item => ({
productId: item.productId,
quantity: item.quantity,
@@ -159,20 +173,20 @@ export default function CreatePurchaseOrder({
return (
<AuthenticatedLayout breadcrumbs={order ? getEditBreadcrumbs("purchaseOrders") : getCreateBreadcrumbs("purchaseOrders")}>
<Head title={order ? "編輯採購單" : "建立採購單"} />
<div className="container mx-auto p-6 max-w-5xl">
<div className="container mx-auto p-6 max-w-7xl">
{/* Header */}
<div className="mb-8">
<div className="mb-6">
<Link href="/purchase-orders">
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
className="gap-2 button-outlined-primary mb-4"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="mb-6">
<div className="mb-4">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<ShoppingCart className="h-6 w-6 text-primary-main" />
{order ? "編輯採購單" : "建立採購單"}
@@ -183,7 +197,7 @@ export default function CreatePurchaseOrder({
</div>
</div>
<div className="space-y-8">
<div className="space-y-6">
{/* 步驟一:基本資訊 */}
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
<div className="p-6 bg-gray-50/50 border-b flex items-center gap-3">
@@ -191,7 +205,7 @@ export default function CreatePurchaseOrder({
<h2 className="text-lg font-bold"></h2>
</div>
<div className="p-8 space-y-8">
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-3">
<label className="text-sm font-bold text-gray-700">
@@ -267,7 +281,7 @@ export default function CreatePurchaseOrder({
<span className="text-sm text-gray-500"></span>
</div>
<div className="p-8 space-y-8">
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="space-y-3">
<label className="text-sm font-bold text-gray-700">
@@ -335,7 +349,7 @@ export default function CreatePurchaseOrder({
</Button>
</div>
<div className="p-8">
<div className="p-6">
{!hasSupplier && (
<Alert className="mb-6 bg-amber-50 border-amber-200 text-amber-800">
<Info className="h-4 w-4 text-amber-600" />
@@ -355,10 +369,53 @@ export default function CreatePurchaseOrder({
/>
{hasSupplier && items.length > 0 && (
<div className="mt-8 flex justify-end">
<div className="bg-primary/5 px-8 py-5 rounded-xl border border-primary/10 inline-flex flex-col items-end min-w-[240px]">
<span className="text-sm text-gray-500 font-medium mb-1"></span>
<span className="text-3xl font-black text-primary">{formatCurrency(totalAmount)}</span>
<div className="mt-6 flex justify-end">
<div className="w-full max-w-sm bg-primary/5 px-6 py-4 rounded-xl border border-primary/10 flex flex-col gap-3">
<div className="flex justify-between items-center w-full">
<span className="text-sm text-gray-500 font-medium"></span>
<span className="text-lg font-bold text-gray-700">{formatCurrency(totalAmount)}</span>
</div>
<div className="flex justify-between items-center w-full gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500 font-medium"> (5%)</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-gray-400 hover:text-primary"
title="重設為自動計算 (5%)"
onClick={() => {
const autoTax = Math.round(totalAmount * 0.05);
setTaxAmount(autoTax);
setIsTaxManual(false);
toast.success("已重設為自動計算 (5%)");
}}
>
</Button>
</div>
<div className="relative w-32">
<Input
type="number"
value={taxAmount}
onChange={(e) => {
setTaxAmount(e.target.value);
setIsTaxManual(true);
}}
className="text-right h-9 bg-white"
placeholder="0"
/>
</div>
</div>
<div className="h-px bg-primary/10 w-full my-1"></div>
<div className="flex justify-between items-end w-full">
<span className="text-sm text-gray-500 font-medium mb-1"> ()</span>
<span className="text-2xl font-black text-primary">
{formatCurrency(totalAmount + (Number(taxAmount) || 0))}
</span>
</div>
</div>
</div>
)}
@@ -367,9 +424,9 @@ export default function CreatePurchaseOrder({
</div>
{/* 底部按鈕 */}
<div className="flex items-center justify-end gap-4 py-8 border-t border-gray-100 mt-8">
<div className="flex items-center justify-end gap-4 py-6 border-t border-gray-100 mt-6">
<Link href="/purchase-orders">
<Button variant="ghost" className="h-12 px-8 text-gray-500 hover:text-gray-700">
<Button variant="ghost" className="h-11 px-6 text-gray-500 hover:text-gray-700">
</Button>
</Link>

View File

@@ -143,11 +143,21 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
items={order.items}
isReadOnly={true}
/>
<div className="mt-4 flex justify-end items-center gap-4 border-t pt-4">
<span className="text-gray-600 font-medium"></span>
<span className="text-xl font-bold text-primary">
{formatCurrency(order.totalAmount)}
</span>
<div className="mt-4 flex flex-col items-end gap-2 border-t pt-4">
<div className="flex items-center gap-8 text-gray-600">
<span className="font-medium"></span>
<span>{formatCurrency(order.totalAmount)}</span>
</div>
<div className="flex items-center gap-8 text-gray-600">
<span className="font-medium"></span>
<span>{formatCurrency(order.tax_amount || 0)}</span>
</div>
<div className="flex items-center gap-8 pt-2 mt-2 border-t border-gray-100">
<span className="font-bold text-lg"></span>
<span className="text-xl font-bold text-primary">
{formatCurrency(order.grand_total || (order.totalAmount + (order.tax_amount || 0)))}
</span>
</div>
</div>
</div>
</div>