first commit
This commit is contained in:
295
resources/js/Pages/PurchaseOrder/Create.tsx
Normal file
295
resources/js/Pages/PurchaseOrder/Create.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* 建立/編輯採購單頁面
|
||||
*/
|
||||
|
||||
import { ArrowLeft, Plus, Info } from "lucide-react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Alert, AlertDescription } from "@/Components/ui/alert";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, Link, router } from "@inertiajs/react";
|
||||
import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable";
|
||||
import type { PurchaseOrder, Supplier } from "@/types/purchase-order";
|
||||
import type { Warehouse } from "@/types/requester";
|
||||
import { usePurchaseOrderForm } from "@/hooks/usePurchaseOrderForm";
|
||||
import {
|
||||
validatePurchaseOrder,
|
||||
filterValidItems,
|
||||
calculateTotalAmount,
|
||||
getTodayDate,
|
||||
formatCurrency,
|
||||
} from "@/utils/purchase-order";
|
||||
import { STATUS_OPTIONS } from "@/constants/purchase-order";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
order?: PurchaseOrder;
|
||||
suppliers: Supplier[];
|
||||
warehouses: Warehouse[];
|
||||
}
|
||||
|
||||
export default function CreatePurchaseOrder({
|
||||
order,
|
||||
suppliers,
|
||||
warehouses,
|
||||
}: Props) {
|
||||
const {
|
||||
supplierId,
|
||||
expectedDate,
|
||||
items,
|
||||
notes,
|
||||
selectedSupplier,
|
||||
isOrderSent,
|
||||
warehouseId,
|
||||
setSupplierId,
|
||||
setExpectedDate,
|
||||
setNotes,
|
||||
setWarehouseId,
|
||||
addItem,
|
||||
removeItem,
|
||||
updateItem,
|
||||
status,
|
||||
setStatus,
|
||||
} = usePurchaseOrderForm({ order, suppliers });
|
||||
|
||||
const totalAmount = calculateTotalAmount(items);
|
||||
const isValid = validatePurchaseOrder(String(supplierId), expectedDate, items);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!isValid || !warehouseId) {
|
||||
toast.error("請填寫完整的表單資訊");
|
||||
return;
|
||||
}
|
||||
|
||||
const validItems = filterValidItems(items);
|
||||
if (validItems.length === 0) {
|
||||
toast.error("請至少新增一項採購商品");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
vendor_id: supplierId,
|
||||
warehouse_id: warehouseId,
|
||||
expected_delivery_date: expectedDate,
|
||||
remark: notes,
|
||||
status: status,
|
||||
items: validItems.map(item => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.unitPrice,
|
||||
})),
|
||||
};
|
||||
|
||||
if (order) {
|
||||
// Edit not implemented yet but structure is ready
|
||||
router.put(`/purchase-orders/${order.id}`, data, {
|
||||
onSuccess: () => toast.success("採購單已更新")
|
||||
});
|
||||
} else {
|
||||
router.post("/purchase-orders", data, {
|
||||
onSuccess: () => toast.success("採購單已成功建立")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasSupplier = !!supplierId;
|
||||
const canSave = isValid && !!warehouseId && items.length > 0;
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<Head title={order ? "編輯採購單" : "建立採購單"} />
|
||||
<div className="container mx-auto p-6 max-w-5xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link href="/purchase-orders">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 button-outlined-primary mb-6"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回列表
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2">{order ? "編輯採購單" : "建立採購單"}</h1>
|
||||
<p className="text-gray-600">
|
||||
{order ? `修改採購單 ${order.poNumber} 的詳細資訊` : "填寫新採購單的資訊以開始流程"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* 步驟一:基本資訊 */}
|
||||
<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">
|
||||
<div className="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold">1</div>
|
||||
<h2 className="text-lg font-bold">基本資訊</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-8 space-y-8">
|
||||
<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">
|
||||
預計入庫倉庫
|
||||
</label>
|
||||
<Select
|
||||
value={String(warehouseId)}
|
||||
onValueChange={setWarehouseId}
|
||||
disabled={isOrderSent}
|
||||
>
|
||||
<SelectTrigger className="h-12 border-gray-200 focus:ring-primary/20">
|
||||
<SelectValue placeholder="請選擇倉庫" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouses.map((w) => (
|
||||
<SelectItem key={w.id} value={String(w.id)}>
|
||||
{w.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-gray-700">供應商</label>
|
||||
<Select
|
||||
value={String(supplierId)}
|
||||
onValueChange={setSupplierId}
|
||||
disabled={isOrderSent}
|
||||
>
|
||||
<SelectTrigger className="h-12 border-gray-200">
|
||||
<SelectValue placeholder="選擇供應商" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{suppliers.map((s) => (
|
||||
<SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
預計到貨日期
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={expectedDate || ""}
|
||||
onChange={(e) => setExpectedDate(e.target.value)}
|
||||
min={getTodayDate()}
|
||||
className="h-12 border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{order && (
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-gray-700">狀態</label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v as any)}
|
||||
>
|
||||
<SelectTrigger className="h-12 border-gray-200">
|
||||
<SelectValue placeholder="選擇狀態" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-gray-700">備註事項</label>
|
||||
<Textarea
|
||||
value={notes || ""}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="備註這筆採購單的特殊需求..."
|
||||
className="min-h-[100px] border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 步驟二:品項明細 */}
|
||||
<div className={`bg-white rounded-lg border shadow-sm overflow-hidden transition-all duration-300 ${!hasSupplier ? 'opacity-60 saturate-50' : ''}`}>
|
||||
<div className="p-6 bg-gray-50/50 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold">2</div>
|
||||
<h2 className="text-lg font-bold">採購商品明細</h2>
|
||||
</div>
|
||||
<Button
|
||||
onClick={addItem}
|
||||
disabled={!hasSupplier || isOrderSent}
|
||||
className="button-filled-primary h-10 gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" /> 新增一個品項
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
{!hasSupplier && (
|
||||
<Alert className="mb-6 bg-amber-50 border-amber-200 text-amber-800">
|
||||
<Info className="h-4 w-4 text-amber-600" />
|
||||
<AlertDescription>
|
||||
請先在步驟一選擇「供應商」,才能從該供應商的常用項目中選取商品。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<PurchaseOrderItemsTable
|
||||
items={items}
|
||||
supplier={selectedSupplier}
|
||||
isReadOnly={isOrderSent}
|
||||
isDisabled={!hasSupplier}
|
||||
onRemoveItem={removeItem}
|
||||
onItemChange={updateItem}
|
||||
/>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部按鈕 */}
|
||||
<div className="flex items-center justify-end gap-4 py-4">
|
||||
<Link href="/purchase-orders">
|
||||
<Button variant="ghost" className="h-12 px-8 text-gray-500 hover:text-gray-700">
|
||||
取消
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary/90 text-white px-12 h-14 rounded-xl shadow-lg shadow-primary/20 text-lg font-bold transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||
onClick={handleSave}
|
||||
disabled={!canSave}
|
||||
>
|
||||
{order ? "更新採購單" : "確認發布採購單"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
132
resources/js/Pages/PurchaseOrder/Index.tsx
Normal file
132
resources/js/Pages/PurchaseOrder/Index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 採購單管理主頁面
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router } from "@inertiajs/react";
|
||||
import PurchaseOrderTable from "@/Components/PurchaseOrder/PurchaseOrderTable";
|
||||
import { PurchaseOrderFilters } from "@/Components/PurchaseOrder/PurchaseOrderFilters";
|
||||
import { type DateRange } from "@/Components/PurchaseOrder/DateFilter";
|
||||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||||
import { debounce } from "lodash";
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
|
||||
interface Props {
|
||||
orders: {
|
||||
data: PurchaseOrder[];
|
||||
links: any[];
|
||||
total: number;
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
filters: {
|
||||
search?: string;
|
||||
status?: string;
|
||||
warehouse_id?: string;
|
||||
sort_field?: string;
|
||||
sort_direction?: string;
|
||||
};
|
||||
warehouses: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
export default function PurchaseOrderIndex({ orders, filters, warehouses }: Props) {
|
||||
const [searchQuery, setSearchQuery] = useState(filters.search || "");
|
||||
const [statusFilter, setStatusFilter] = useState<string>(filters.status || "all");
|
||||
const [requesterFilter, setRequesterFilter] = useState<string>(filters.warehouse_id || "all");
|
||||
const [dateRange, setDateRange] = useState<DateRange | null>(null);
|
||||
|
||||
const handleFilterChange = (newFilters: any) => {
|
||||
router.get("/purchase-orders", {
|
||||
...filters,
|
||||
...newFilters,
|
||||
page: 1,
|
||||
}, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
debounce((value: string) => {
|
||||
handleFilterChange({ search: value });
|
||||
}, 500),
|
||||
[filters]
|
||||
);
|
||||
|
||||
const onSearchChange = (value: string) => {
|
||||
setSearchQuery(value);
|
||||
handleSearch(value);
|
||||
};
|
||||
|
||||
const onStatusChange = (value: string) => {
|
||||
setStatusFilter(value);
|
||||
handleFilterChange({ status: value });
|
||||
};
|
||||
|
||||
const onWarehouseChange = (value: string) => {
|
||||
setRequesterFilter(value);
|
||||
handleFilterChange({ warehouse_id: value });
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setSearchQuery("");
|
||||
setStatusFilter("all");
|
||||
setRequesterFilter("all");
|
||||
setDateRange(null);
|
||||
router.get("/purchase-orders");
|
||||
};
|
||||
|
||||
const hasActiveFilters = searchQuery !== "" || statusFilter !== "all" || requesterFilter !== "all" || dateRange !== null;
|
||||
|
||||
const handleNavigateToCreateOrder = () => {
|
||||
router.get("/purchase-orders/create");
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<Head title="採購管理 - 管理採購單" />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="mb-2">管理採購單</h1>
|
||||
<p className="text-gray-600">追蹤並管理所有倉庫的採購申請與進度</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleNavigateToCreateOrder}
|
||||
className="gap-2 button-filled-primary"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
建立採購單
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<PurchaseOrderFilters
|
||||
searchQuery={searchQuery}
|
||||
statusFilter={statusFilter}
|
||||
requesterFilter={requesterFilter}
|
||||
warehouses={warehouses}
|
||||
onSearchChange={onSearchChange}
|
||||
onStatusChange={onStatusChange}
|
||||
onRequesterChange={onWarehouseChange}
|
||||
onClearFilters={handleClearFilters}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
dateRange={dateRange}
|
||||
onDateRangeChange={setDateRange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PurchaseOrderTable
|
||||
orders={orders.data}
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Pagination links={orders.links} />
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
173
resources/js/Pages/PurchaseOrder/Show.tsx
Normal file
173
resources/js/Pages/PurchaseOrder/Show.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 查看採購單詳情頁面
|
||||
*/
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, Link } from "@inertiajs/react";
|
||||
import { StatusProgressBar } from "@/Components/PurchaseOrder/StatusProgressBar";
|
||||
import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderStatusBadge";
|
||||
import CopyButton from "@/Components/shared/CopyButton";
|
||||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||||
import { formatCurrency, formatDateTime } from "@/utils/format";
|
||||
|
||||
interface Props {
|
||||
order: PurchaseOrder;
|
||||
}
|
||||
|
||||
export default function ViewPurchaseOrderPage({ order }: Props) {
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<Head title={`採購單詳情 - ${order.poNumber}`} />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<Link href="/purchase-orders">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 button-outlined-primary mb-6"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回採購單列表
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="mb-2">查看採購單</h1>
|
||||
<p className="text-gray-600">單號:{order.poNumber}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/purchase-orders/${order.id}/edit`}>
|
||||
<Button variant="outline" className="button-outlined-primary">
|
||||
編輯採購單
|
||||
</Button>
|
||||
</Link>
|
||||
<PurchaseOrderStatusBadge status={order.status} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 狀態流程條 */}
|
||||
<div className="mb-8">
|
||||
<StatusProgressBar currentStatus={order.status} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* 基本資訊與品項 */}
|
||||
<div className="space-y-8">
|
||||
|
||||
|
||||
{/* 基本資訊卡片 */}
|
||||
<div className="bg-white rounded-lg border shadow-sm p-6">
|
||||
<h2 className="text-lg font-bold text-gray-900 mb-6">基本資訊</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">採購單編號</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono font-medium text-gray-900">{order.poNumber}</span>
|
||||
<CopyButton text={order.poNumber} label="複製單號" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">供應商</span>
|
||||
<span className="font-medium text-gray-900">{order.supplierName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">申請單位 (申請人)</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{order.warehouse_name} ({order.createdBy})
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">建立日期</span>
|
||||
<span className="font-medium text-gray-900">{formatDateTime(order.createdAt)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">預計到貨日期</span>
|
||||
<span className="font-medium text-gray-900">{order.expectedDate || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
{order.remark && (
|
||||
<div className="mt-8 pt-6 border-t border-gray-100">
|
||||
<span className="text-sm text-gray-500 block mb-2">備註</span>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-4 rounded-lg leading-relaxed">
|
||||
{order.remark}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 採購項目卡片 */}
|
||||
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-100">
|
||||
<h2 className="text-lg font-bold text-gray-900">採購項目清單</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50/50">
|
||||
<th className="text-left py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider w-[50px]">
|
||||
#
|
||||
</th>
|
||||
<th className="text-left py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
商品名稱
|
||||
</th>
|
||||
<th className="text-right py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
單價
|
||||
</th>
|
||||
<th className="text-right py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider w-32">
|
||||
數量
|
||||
</th>
|
||||
<th className="text-right py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
小計
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{order.items.map((item, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50/30 transition-colors">
|
||||
<td className="py-4 px-6 text-gray-500 font-medium text-center">
|
||||
{index + 1}
|
||||
</td>
|
||||
<td className="py-4 px-6">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-gray-900">{item.productName}</span>
|
||||
<span className="text-xs text-gray-400">ID: {item.productId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-right">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-gray-900">{formatCurrency(item.unitPrice)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-right">
|
||||
<span className="text-gray-900 font-medium">
|
||||
{item.quantity} {item.unit}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-right font-bold text-gray-900">
|
||||
{formatCurrency(item.subtotal)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot className="bg-gray-50/50 border-t border-gray-100">
|
||||
<tr>
|
||||
<td colSpan={4} className="py-5 px-6 text-right font-medium text-gray-600">
|
||||
總金額
|
||||
</td>
|
||||
<td className="py-5 px-6 text-right font-bold text-xl text-primary">
|
||||
{formatCurrency(order.totalAmount)}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user