大更新
This commit is contained in:
@@ -9,6 +9,7 @@ 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 { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable";
|
||||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||||
import { formatCurrency, formatDateTime } from "@/utils/format";
|
||||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||||
@@ -104,66 +105,17 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
|
||||
<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 className="p-6">
|
||||
<PurchaseOrderItemsTable
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
38
resources/js/Pages/Vendor/Show.tsx
vendored
38
resources/js/Pages/Vendor/Show.tsx
vendored
@@ -33,8 +33,14 @@ interface VendorProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
unit?: string;
|
||||
base_unit?: string;
|
||||
// Relations might be camelCase or snake_case depending on serialization settings
|
||||
baseUnit?: { name: string };
|
||||
base_unit?: { name: string };
|
||||
largeUnit?: { name: string };
|
||||
large_unit?: { name: string };
|
||||
purchaseUnit?: string; // Note: if it's a relation it might be an object, but original code treated it as string
|
||||
purchase_unit?: string;
|
||||
conversion_rate?: number;
|
||||
pivot: Pivot;
|
||||
}
|
||||
|
||||
@@ -54,13 +60,29 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
const [selectedProduct, setSelectedProduct] = useState<SupplyProduct | null>(null);
|
||||
|
||||
// 轉換後端資料格式為前端組件需要的格式
|
||||
const supplyProducts: SupplyProduct[] = vendor.products.map(p => ({
|
||||
id: String(p.id),
|
||||
productId: String(p.id),
|
||||
productName: p.name,
|
||||
unit: p.purchase_unit || p.base_unit || "個",
|
||||
lastPrice: p.pivot.last_price || undefined,
|
||||
}));
|
||||
const supplyProducts: SupplyProduct[] = vendor.products.map(p => {
|
||||
// Laravel load('relationName') usually results in camelCase key in JSON if method is camelCase
|
||||
const baseUnitName = p.baseUnit?.name || p.base_unit?.name;
|
||||
const largeUnitName = p.largeUnit?.name || p.large_unit?.name;
|
||||
|
||||
// Check purchase unit - seemingly originally a field string, but if relation, check if object
|
||||
// Assuming purchase_unit is a string field on product table here based on original code usage?
|
||||
// Wait, original code usage: p.purchase_unit || ...
|
||||
// In Product model: purchase_unit_id exists, purchaseUnit is relation.
|
||||
// If p.purchase_unit was working before, it might be an attribute (accessors).
|
||||
// Let's stick to safe access.
|
||||
|
||||
return {
|
||||
id: String(p.id),
|
||||
productId: String(p.id),
|
||||
productName: p.name,
|
||||
unit: p.purchase_unit || baseUnitName || "個",
|
||||
baseUnit: baseUnitName,
|
||||
largeUnit: largeUnitName,
|
||||
conversionRate: p.conversion_rate,
|
||||
lastPrice: p.pivot.last_price || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const handleAddProduct = (productId: string, lastPrice?: number) => {
|
||||
router.post(route('vendors.products.store', vendor.id), {
|
||||
|
||||
@@ -33,7 +33,9 @@ import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
|
||||
interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
unit: string;
|
||||
baseUnit: string;
|
||||
largeUnit?: string;
|
||||
conversionRate?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -58,13 +60,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
|
||||
// 新增明細行
|
||||
const handleAddItem = () => {
|
||||
const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", unit: "kg" };
|
||||
const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", baseUnit: "個" };
|
||||
const newItem: InboundItem = {
|
||||
tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
productId: defaultProduct.id,
|
||||
productName: defaultProduct.name,
|
||||
quantity: 0,
|
||||
unit: defaultProduct.unit,
|
||||
unit: defaultProduct.baseUnit, // 僅用於顯示當前選擇單位的名稱
|
||||
baseUnit: defaultProduct.baseUnit,
|
||||
largeUnit: defaultProduct.largeUnit,
|
||||
conversionRate: defaultProduct.conversionRate,
|
||||
selectedUnit: 'base',
|
||||
};
|
||||
setItems([...items, newItem]);
|
||||
};
|
||||
@@ -86,11 +92,16 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
// 處理商品變更
|
||||
const handleProductChange = (tempId: string, productId: string) => {
|
||||
const product = products.find((p) => p.id === productId);
|
||||
|
||||
if (product) {
|
||||
handleUpdateItem(tempId, {
|
||||
productId,
|
||||
productName: product.name,
|
||||
unit: product.unit,
|
||||
unit: product.baseUnit,
|
||||
baseUnit: product.baseUnit,
|
||||
largeUnit: product.largeUnit,
|
||||
conversionRate: product.conversionRate,
|
||||
selectedUnit: 'base',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -135,10 +146,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
inboundDate,
|
||||
reason,
|
||||
notes,
|
||||
items: items.map(item => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity
|
||||
}))
|
||||
items: items.map(item => {
|
||||
// 如果選擇大單位,則換算為基本單位數量
|
||||
const finalQuantity = item.selectedUnit === 'large' && item.conversionRate
|
||||
? item.quantity * item.conversionRate
|
||||
: item.quantity;
|
||||
|
||||
return {
|
||||
productId: item.productId,
|
||||
quantity: finalQuantity
|
||||
};
|
||||
})
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success("庫存記錄已儲存");
|
||||
@@ -296,71 +314,106 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
數量 <span className="text-red-500">*</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">單位</TableHead>
|
||||
<TableHead className="w-[150px]">轉換數量</TableHead>
|
||||
{/* <TableHead className="w-[180px]">效期</TableHead>
|
||||
<TableHead className="w-[220px]">進貨編號</TableHead> */}
|
||||
<TableHead className="w-[60px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item, index) => (
|
||||
<TableRow key={item.tempId}>
|
||||
{/* 商品 */}
|
||||
<TableCell>
|
||||
<Select
|
||||
value={item.productId}
|
||||
onValueChange={(value) =>
|
||||
handleProductChange(item.tempId, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="border-gray-300">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{products.map((product) => (
|
||||
<SelectItem key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors[`item-${index}-product`] && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors[`item-${index}-product`]}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
{items.map((item, index) => {
|
||||
// 計算轉換數量
|
||||
const convertedQuantity = item.selectedUnit === 'large' && item.conversionRate
|
||||
? item.quantity * item.conversionRate
|
||||
: item.quantity;
|
||||
|
||||
{/* 數量 */}
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={item.quantity || ""}
|
||||
onChange={(e) =>
|
||||
handleUpdateItem(item.tempId, {
|
||||
quantity: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="border-gray-300"
|
||||
/>
|
||||
{errors[`item-${index}-quantity`] && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors[`item-${index}-quantity`]}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
return (
|
||||
<TableRow key={item.tempId}>
|
||||
{/* 商品 */}
|
||||
<TableCell>
|
||||
<Select
|
||||
value={item.productId}
|
||||
onValueChange={(value) =>
|
||||
handleProductChange(item.tempId, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="border-gray-300">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[9999]">
|
||||
{products.map((product) => (
|
||||
<SelectItem key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors[`item-${index}-product`] && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors[`item-${index}-product`]}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 單位 */}
|
||||
<TableCell>
|
||||
<Input
|
||||
value={item.unit}
|
||||
disabled
|
||||
className="bg-gray-50 border-gray-200"
|
||||
/>
|
||||
</TableCell>
|
||||
{/* 數量 */}
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={item.quantity || ""}
|
||||
onChange={(e) =>
|
||||
handleUpdateItem(item.tempId, {
|
||||
quantity: parseFloat(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="border-gray-300"
|
||||
/>
|
||||
{errors[`item-${index}-quantity`] && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors[`item-${index}-quantity`]}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 效期 */}
|
||||
{/* <TableCell>
|
||||
{/* 單位 */}
|
||||
<TableCell>
|
||||
{item.largeUnit ? (
|
||||
<Select
|
||||
value={item.selectedUnit}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateItem(item.tempId, {
|
||||
selectedUnit: value as 'base' | 'large',
|
||||
unit: value === 'base' ? item.baseUnit : item.largeUnit
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="border-gray-300">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[9999]">
|
||||
<SelectItem value="base">{item.baseUnit}</SelectItem>
|
||||
<SelectItem value="large">{item.largeUnit}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={item.baseUnit || "個"}
|
||||
disabled
|
||||
className="bg-gray-50 border-gray-200"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 轉換數量 */}
|
||||
<TableCell>
|
||||
<div className="flex items-center text-gray-700 font-medium bg-gray-50 px-3 py-2 rounded-md border border-gray-200">
|
||||
<span>{convertedQuantity}</span>
|
||||
<span className="ml-1 text-gray-500 text-sm">{item.baseUnit || "個"}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* 效期 */}
|
||||
{/* <TableCell>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="date"
|
||||
@@ -375,8 +428,8 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
</div>
|
||||
</TableCell> */}
|
||||
|
||||
{/* 批號 */}
|
||||
{/* <TableCell>
|
||||
{/* 批號 */}
|
||||
{/* <TableCell>
|
||||
<Input
|
||||
value={item.batchNumber}
|
||||
onChange={(e) =>
|
||||
@@ -392,20 +445,21 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
)}
|
||||
</TableCell> */}
|
||||
|
||||
{/* 刪除按鈕 */}
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveItem(item.tempId)}
|
||||
className="hover:bg-red-50 hover:text-red-600 h-8 w-8"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{/* 刪除按鈕 */}
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveItem(item.tempId)}
|
||||
className="hover:bg-red-50 hover:text-red-600 h-8 w-8"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -78,9 +78,17 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) {
|
||||
};
|
||||
|
||||
const handleDeleteWarehouse = (id: string) => {
|
||||
if (confirm("確定要停用此倉庫嗎?\n注意:刪除倉庫將連帶刪除所有庫存與紀錄!")) {
|
||||
router.delete(route('warehouses.destroy', id));
|
||||
}
|
||||
router.delete(route('warehouses.destroy', id), {
|
||||
onSuccess: () => {
|
||||
toast.success('倉庫已刪除');
|
||||
setEditingWarehouse(null);
|
||||
},
|
||||
onError: (errors: any) => {
|
||||
// If backend returns error bag or flash error
|
||||
// Flash error is handled by AuthenticatedLayout usually via usePage props.
|
||||
// But we can also check errors bag here if needed.
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddTransferOrder = () => {
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function WarehouseInventoryPage({
|
||||
|
||||
// 導航至流動紀錄頁
|
||||
const handleView = (inventoryId: string) => {
|
||||
router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventory: inventoryId }));
|
||||
router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventoryId: inventoryId }));
|
||||
};
|
||||
|
||||
|
||||
@@ -74,13 +74,17 @@ export default function WarehouseInventoryPage({
|
||||
const handleDelete = () => {
|
||||
if (!deleteId) return;
|
||||
|
||||
router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventory: deleteId }), {
|
||||
// 暫存 ID 以免在對話框關閉的瞬間 state 被清空
|
||||
const idToDelete = deleteId;
|
||||
|
||||
router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventoryId: idToDelete }), {
|
||||
onSuccess: () => {
|
||||
toast.success("庫存記錄已刪除");
|
||||
setDeleteId(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("刪除失敗");
|
||||
// 保持對話框開啟以便重試,或根據需要關閉
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -112,7 +116,7 @@ export default function WarehouseInventoryPage({
|
||||
{/* 操作按鈕 (位於標題下方) */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
{/* 安全庫存設定按鈕 */}
|
||||
<Link href={`/warehouses/${warehouse.id}/safety-stock-settings`}>
|
||||
<Link href={route('warehouses.safety-stock.index', warehouse.id)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="button-outlined-primary"
|
||||
@@ -135,7 +139,7 @@ export default function WarehouseInventoryPage({
|
||||
</Button>
|
||||
|
||||
{/* 新增庫存按鈕 */}
|
||||
<Link href={`/warehouses/${warehouse.id}/add-inventory`}>
|
||||
<Link href={route('warehouses.inventory.create', warehouse.id)}>
|
||||
<Button
|
||||
className="button-filled-primary"
|
||||
>
|
||||
@@ -163,9 +167,6 @@ export default function WarehouseInventoryPage({
|
||||
onDelete={confirmDelete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 刪除確認對話框 */}
|
||||
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -176,7 +177,12 @@ export default function WarehouseInventoryPage({
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="button-outlined-primary">取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700 text-white">
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
handleDelete();
|
||||
}}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
確認刪除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user