feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
This commit is contained in:
@@ -168,6 +168,28 @@ const fieldLabels: Record<string, string> = {
|
||||
posted_by: '過帳者',
|
||||
counted_qty: '盤點數量',
|
||||
adjust_qty: '調整數量',
|
||||
// 調撥單專有欄位
|
||||
transit_warehouse_id: '在途倉庫',
|
||||
transit_warehouse_name: '在途倉庫名稱',
|
||||
dispatched_at: '出貨日期',
|
||||
dispatched_by: '出貨人',
|
||||
received_at: '收貨日期',
|
||||
received_by: '收貨人',
|
||||
reserved_quantity: '預扣數量',
|
||||
snapshot_quantity: '異動前庫存 (快照)',
|
||||
// 門市叫貨欄位
|
||||
store_warehouse_id: '申請倉庫',
|
||||
store_warehouse_name: '申請倉庫',
|
||||
supply_warehouse_id: '供貨倉庫',
|
||||
supply_warehouse_name: '供貨倉庫',
|
||||
approved_by: '審核人',
|
||||
approved_user_name: '審核人',
|
||||
approved_at: '審核時間',
|
||||
submitted_at: '提交時間',
|
||||
reject_reason: '駁回原因',
|
||||
status_label: '處理狀態',
|
||||
transfer_order_id: '調撥單 ID',
|
||||
transfer_order_name: '調撥單號',
|
||||
};
|
||||
|
||||
// 狀態翻譯對照表
|
||||
@@ -192,6 +214,7 @@ const statusMap: Record<string, string> = {
|
||||
in_progress: '生產中',
|
||||
// 調撥單狀態
|
||||
voided: '已作廢',
|
||||
dispatched: '已出貨',
|
||||
};
|
||||
|
||||
// 主體類型解析 (Model 類名轉中文)
|
||||
@@ -206,17 +229,7 @@ const subjectTypeMap: Record<string, string> = {
|
||||
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
|
||||
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
|
||||
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
|
||||
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
|
||||
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
|
||||
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
|
||||
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
|
||||
'App\\Modules\\Core\\Models\\User': '使用者帳號',
|
||||
'App\\Modules\\Core\\Models\\Role': '角色權限',
|
||||
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
|
||||
// 簡寫映射 (應對後端回傳 class_basename 的情況)
|
||||
'Product': '商品資料',
|
||||
'Warehouse': '倉庫資料',
|
||||
@@ -231,7 +244,8 @@ const subjectTypeMap: Record<string, string> = {
|
||||
'Recipe': '生產配方',
|
||||
'InventoryCountDoc': '庫存盤點單',
|
||||
'InventoryAdjustDoc': '庫存盤調單',
|
||||
'InventoryTransferOrder': '庫存調撥單',
|
||||
'InventoryTransferOrder': '庫庫調撥單',
|
||||
'StoreRequisition': '門市叫貨單',
|
||||
'StockMovementDoc': '庫存單據',
|
||||
'User': '使用者帳號',
|
||||
'Role': '角色權限',
|
||||
@@ -264,17 +278,31 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
|
||||
// 自訂欄位排序順序
|
||||
const sortOrder = [
|
||||
'doc_no', 'po_number', 'gr_number', 'production_number',
|
||||
'doc_no', 'po_number', 'gr_number', 'production_number', 'transfer_order_name',
|
||||
'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
|
||||
'invoice_number', 'invoice_date', 'invoice_amount',
|
||||
'total_amount', 'tax_amount', 'grand_total'
|
||||
];
|
||||
|
||||
// 過濾掉通常會記錄但對使用者無用的內部鍵
|
||||
// 過濾掉通常會記錄但對使用者無用的內部鍵,以及已被解析為名稱的原始 ID 欄位
|
||||
const filteredKeys = allKeys
|
||||
.filter(key =>
|
||||
!['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token'].includes(key)
|
||||
)
|
||||
.filter(key => {
|
||||
if (['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token', 'activityProperties'].includes(key)) return false;
|
||||
|
||||
// 隱藏冗餘的狀態標籤 (因為後端已統一替換 status 內容)
|
||||
if (key === 'status_label') return false;
|
||||
|
||||
// 隱藏技術用的 ID 欄位 (如果已有對應的名稱欄位)
|
||||
if (key.endsWith('_id')) {
|
||||
const nameKey = key.replace('_id', '_name');
|
||||
const userNameKey = key.replace('_id', '_user_name');
|
||||
if (allKeys.includes(nameKey) || allKeys.includes(userNameKey)) return false;
|
||||
}
|
||||
// 特別隱藏調撥單 ID
|
||||
if (key === 'transfer_order_id' && (allKeys.includes('transfer_order_name') || allKeys.includes('transfer_order_doc_no'))) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const indexA = sortOrder.indexOf(a);
|
||||
const indexB = sortOrder.indexOf(b);
|
||||
@@ -336,9 +364,13 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') {
|
||||
return value.split('T')[0].split(' ')[0];
|
||||
}
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time') && typeof value === 'string') {
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time' || key === 'submitted_at' || key === 'approved_at') && typeof value === 'string') {
|
||||
try {
|
||||
const date = new Date(value);
|
||||
// 處理部分 ISO 字串包含 T 的情況
|
||||
const normalizedValue = value.replace('T', ' ');
|
||||
const date = new Date(normalizedValue);
|
||||
if (isNaN(date.getTime())) return value;
|
||||
|
||||
return date.toLocaleString('zh-TW', {
|
||||
timeZone: 'Asia/Taipei',
|
||||
year: 'numeric',
|
||||
@@ -537,65 +569,89 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{/* 更新項目 */}
|
||||
{activity.properties.items_diff.updated?.map((item: any, idx: number) => (
|
||||
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
|
||||
<TableCell className="font-medium">{item.product_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">更新</Badge>
|
||||
{/* 1. 處理物件格式的 items_diff (現有的採購、盤點、調撥初始紀錄格式) */}
|
||||
{!Array.isArray(activity.properties?.items_diff) && (
|
||||
<>
|
||||
{/* 更新項目 */}
|
||||
{activity.properties?.items_diff?.updated?.map((item: any, idx: number) => (
|
||||
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
|
||||
<TableCell className="font-medium">{item.product_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">更新</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
<div className="space-y-1 text-xs">
|
||||
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
|
||||
<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 && item.old?.counted_qty !== undefined && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) || null}
|
||||
|
||||
{/* 新增項目 */}
|
||||
{activity.properties?.items_diff?.added?.map((item: any, idx: number) => (
|
||||
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
|
||||
<TableCell className="font-medium">{item.product_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">新增</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
數量: {item.new?.quantity ?? item.quantity} {item.unit_name || item.new?.unit_name || ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) || null}
|
||||
|
||||
{/* 移除項目 */}
|
||||
{activity.properties?.items_diff?.removed?.map((item: any, idx: number) => (
|
||||
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
|
||||
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">移除</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-400">
|
||||
原數量: {item.old?.quantity ?? item.quantity} {item.unit_name || item.old?.unit_name || ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) || null}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 2. 處理陣列格式的 items_diff (調撥單過帳/收貨的複合紀錄格式) */}
|
||||
{Array.isArray(activity.properties?.items_diff) && activity.properties.items_diff.map((item: any, idx: number) => (
|
||||
<TableRow key={`trf-${idx}`} className="hover:bg-gray-50/50">
|
||||
<TableCell className="font-medium">
|
||||
{item.product_name}
|
||||
<div className="text-[10px] text-gray-400 font-normal">批號: {item.batch_number}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
<div className="space-y-1">
|
||||
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
|
||||
<div>數量: <span className="text-gray-500 line-through">{item.old.quantity}</span> → <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200">異動</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs p-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
{item.source_warehouse && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-16 text-gray-400 truncate">來源 ({item.source_warehouse}):</span>
|
||||
<span className="text-gray-400 line-through">{Number(item.source_before || 0).toFixed(0)}</span>
|
||||
<span className="text-rose-600 font-bold">→ {Number(item.source_after || 0).toFixed(0)}</span>
|
||||
<span className="text-[10px] bg-rose-50 text-rose-600 px-1 rounded">-{item.quantity}</span>
|
||||
</div>
|
||||
)}
|
||||
{item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
|
||||
<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?.adjust_qty !== item.new?.adjust_qty && (
|
||||
<div>調整量: <span className="text-gray-500 line-through">{item.old?.adjust_qty ?? '0'}</span> → <span className="text-blue-700 font-bold">{item.new?.adjust_qty ?? '0'}</span></div>
|
||||
)}
|
||||
{item.old?.unit_name !== item.new?.unit_name && item.old?.unit_name !== undefined && (
|
||||
<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 !== undefined && (
|
||||
<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>
|
||||
{item.target_warehouse && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-16 text-gray-400 truncate">目的 ({item.target_warehouse}):</span>
|
||||
<span className="text-gray-400 line-through">{Number(item.target_before || 0).toFixed(0)}</span>
|
||||
<span className="text-emerald-600 font-bold">→ {Number(item.target_after || 0).toFixed(0)}</span>
|
||||
<span className="text-[10px] bg-emerald-50 text-emerald-600 px-1 rounded">+{item.quantity}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) || null}
|
||||
|
||||
{/* 新增項目 */}
|
||||
{activity.properties.items_diff.added?.map((item: any, idx: number) => (
|
||||
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
|
||||
<TableCell className="font-medium">{item.product_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">新增</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{item.quantity !== undefined ? `數量: ${item.quantity} ${item.unit_name || ''} / ` : ''}
|
||||
{item.adjust_qty !== undefined ? `調整量: ${item.adjust_qty} / ` : ''}
|
||||
{item.subtotal !== undefined ? `小計: $${item.subtotal}` : ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) || null}
|
||||
|
||||
{/* 移除項目 */}
|
||||
{activity.properties.items_diff.removed?.map((item: any, idx: number) => (
|
||||
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
|
||||
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">移除</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-400">
|
||||
原紀錄: {item.quantity} {item.unit_name}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)) || null}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,7 @@ const subjectTypeMap: Record<string, string> = {
|
||||
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
|
||||
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
|
||||
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
|
||||
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
|
||||
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
|
||||
@@ -67,6 +68,7 @@ const subjectTypeMap: Record<string, string> = {
|
||||
'InventoryCountDoc': '庫存盤點單',
|
||||
'InventoryAdjustDoc': '庫存盤調單',
|
||||
'InventoryTransferOrder': '庫存調撥單',
|
||||
'StoreRequisition': '門市叫貨單',
|
||||
'StockMovementDoc': '庫存單據',
|
||||
'User': '使用者帳號',
|
||||
'Role': '角色權限',
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
/**
|
||||
* 新增供貨商品對話框
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/Components/ui/dialog";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/Components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/Components/ui/popover";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Product } from "@/types/product";
|
||||
import type { SupplyProduct } from "@/types/vendor";
|
||||
|
||||
interface AddSupplyProductDialogProps {
|
||||
open: boolean;
|
||||
products: Product[];
|
||||
existingSupplyProducts: SupplyProduct[];
|
||||
onClose: () => void;
|
||||
onAdd: (productId: string, lastPrice?: number) => void;
|
||||
}
|
||||
|
||||
export default function AddSupplyProductDialog({
|
||||
open,
|
||||
products,
|
||||
existingSupplyProducts,
|
||||
onClose,
|
||||
onAdd,
|
||||
}: AddSupplyProductDialogProps) {
|
||||
const [selectedProductId, setSelectedProductId] = useState<string>("");
|
||||
const [lastPrice, setLastPrice] = useState<string>("");
|
||||
const [openCombobox, setOpenCombobox] = useState(false);
|
||||
|
||||
// 過濾掉已經在供貨列表中的商品
|
||||
const availableProducts = useMemo(() => {
|
||||
const existingIds = new Set(existingSupplyProducts.map(sp => String(sp.productId)));
|
||||
return products.filter(p => !existingIds.has(String(p.id)));
|
||||
}, [products, existingSupplyProducts]);
|
||||
|
||||
const selectedProduct = availableProducts.find(p => p.id === selectedProductId);
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!selectedProductId) return;
|
||||
|
||||
const price = lastPrice ? parseFloat(lastPrice) : undefined;
|
||||
onAdd(selectedProductId, price);
|
||||
|
||||
// 重置表單
|
||||
setSelectedProductId("");
|
||||
setLastPrice("");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedProductId("");
|
||||
setLastPrice("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新增供貨商品</DialogTitle>
|
||||
<DialogDescription>選擇該廠商可供應的商品並設定採購價格。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 商品選擇 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm font-medium">商品名稱</Label>
|
||||
<Popover open={openCombobox} onOpenChange={setOpenCombobox}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={openCombobox}
|
||||
className="flex h-9 w-full items-center justify-between rounded-md border-2 border-grey-3 !bg-grey-5 px-3 py-1 text-sm font-normal text-grey-0 text-left outline-none transition-colors hover:!bg-grey-5 hover:border-primary/50 focus-visible:border-[var(--primary-main)] focus-visible:ring-[3px] focus-visible:ring-[var(--primary-main)]/20"
|
||||
onClick={() => setOpenCombobox(!openCombobox)}
|
||||
>
|
||||
{selectedProduct ? (
|
||||
<span className="font-medium text-gray-900">{selectedProduct.name}</span>
|
||||
) : (
|
||||
<span className="text-gray-400">請選擇商品...</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[450px] p-0 shadow-lg border-2 z-[9999]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="搜尋商品名稱..." />
|
||||
<CommandList className="max-h-[300px]">
|
||||
<CommandEmpty className="py-6 text-center text-sm text-gray-500">
|
||||
找不到符合的商品
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableProducts.map((product) => (
|
||||
<CommandItem
|
||||
key={product.id}
|
||||
value={product.name}
|
||||
onSelect={() => {
|
||||
setSelectedProductId(product.id);
|
||||
setOpenCombobox(false);
|
||||
}}
|
||||
className="cursor-pointer aria-selected:bg-primary/5 aria-selected:text-primary py-3"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4 text-primary",
|
||||
selectedProductId === product.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center justify-between flex-1">
|
||||
<span className="font-medium">{product.name}</span>
|
||||
<span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded">
|
||||
{product.baseUnit?.name || (product.base_unit as any)?.name || product.base_unit || "個"}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 單位(自動帶入) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm font-medium text-gray-500">單位</Label>
|
||||
<div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-gray-600 font-medium text-sm flex items-center">
|
||||
{selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 上次採購價格 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">
|
||||
上次採購單價 / {selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "單位"}
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
placeholder="輸入價格"
|
||||
value={lastPrice}
|
||||
onChange={(e) => setLastPrice(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
className="gap-2 button-outlined-primary"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleAdd}
|
||||
disabled={!selectedProductId}
|
||||
className="gap-2 button-filled-primary"
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* 編輯供貨商品對話框
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/Components/ui/dialog";
|
||||
import type { SupplyProduct } from "@/types/vendor";
|
||||
|
||||
interface EditSupplyProductDialogProps {
|
||||
open: boolean;
|
||||
product: SupplyProduct | null;
|
||||
onClose: () => void;
|
||||
onSave: (productId: string, lastPrice?: number) => void;
|
||||
}
|
||||
|
||||
export default function EditSupplyProductDialog({
|
||||
open,
|
||||
product,
|
||||
onClose,
|
||||
onSave,
|
||||
}: EditSupplyProductDialogProps) {
|
||||
const [lastPrice, setLastPrice] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (product) {
|
||||
setLastPrice(product.lastPrice?.toString() || "");
|
||||
}
|
||||
}, [product, open]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!product) return;
|
||||
|
||||
const price = lastPrice ? parseFloat(lastPrice) : undefined;
|
||||
onSave(product.productId, price);
|
||||
setLastPrice("");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setLastPrice("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!product) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>編輯供貨商品</DialogTitle>
|
||||
<DialogDescription>修改商品的採購價格資訊。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 商品名稱(不可編輯) */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">商品名稱</Label>
|
||||
<Input
|
||||
value={product.productName}
|
||||
disabled
|
||||
className="mt-1 bg-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 單位(不可編輯) */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">單位</Label>
|
||||
<Input
|
||||
value={product.unit}
|
||||
disabled
|
||||
className="mt-1 bg-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 上次採購價格 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">上次採購單價 / {product.baseUnit || "單位"}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
placeholder="輸入價格"
|
||||
value={lastPrice}
|
||||
onChange={(e) => setLastPrice(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
className="gap-2 button-outlined-primary"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
className="gap-2 button-filled-primary"
|
||||
>
|
||||
儲存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
155
resources/js/Components/Vendor/SupplyProductList.tsx
vendored
155
resources/js/Components/Vendor/SupplyProductList.tsx
vendored
@@ -1,5 +1,7 @@
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,90 +10,129 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
import type { SupplyProduct } from "@/types/vendor";
|
||||
|
||||
interface SupplyProductListProps {
|
||||
products: SupplyProduct[];
|
||||
onEdit: (product: SupplyProduct) => void;
|
||||
onRemove: (product: SupplyProduct) => void;
|
||||
items: SupplyProduct[];
|
||||
allProducts: any[];
|
||||
onRemoveItem: (index: number) => void;
|
||||
onItemChange: (index: number, field: keyof SupplyProduct, value: string | number) => void;
|
||||
}
|
||||
|
||||
export default function SupplyProductList({
|
||||
products,
|
||||
onEdit,
|
||||
onRemove,
|
||||
items,
|
||||
allProducts,
|
||||
onRemoveItem,
|
||||
onItemChange,
|
||||
}: SupplyProductListProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg border shadow-sm">
|
||||
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableRow className="bg-gray-50 hover:bg-gray-50 text-grey-0">
|
||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||
<TableHead>商品名稱</TableHead>
|
||||
<TableHead>基本單位</TableHead>
|
||||
<TableHead>轉換率</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<TableHead className="w-[40%] text-left">商品名稱</TableHead>
|
||||
<TableHead className="w-[15%] text-left">基本單位</TableHead>
|
||||
<TableHead className="w-[20%] text-left">
|
||||
上次採購單價
|
||||
<div className="text-xs font-normal text-muted-foreground">(以基本單位計算)</div>
|
||||
<span className="text-[10px] font-normal text-muted-foreground block">(以基本單位計)</span>
|
||||
</TableHead>
|
||||
<TableHead className="text-center w-[150px]">操作</TableHead>
|
||||
<TableHead className="text-center w-[80px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products.length === 0 ? (
|
||||
{items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
尚無供貨商品,請點擊上方按鈕新增
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground py-12 italic text-sm">
|
||||
尚未新增任何供貨商品,點擊上方按鈕新增
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
products.map((product, index) => (
|
||||
<TableRow key={product.id}>
|
||||
items.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-gray-500 font-medium text-center">
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<TableCell>{product.productName}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{product.baseUnit || product.unit || "-"}
|
||||
<SearchableSelect
|
||||
value={item.productId}
|
||||
onValueChange={(value) => onItemChange(index, "productId", value)}
|
||||
options={allProducts.map(p => ({
|
||||
label: p.name,
|
||||
value: String(p.id)
|
||||
}))}
|
||||
placeholder="選擇商品"
|
||||
searchPlaceholder="搜尋商品..."
|
||||
className="w-full h-10"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{product.largeUnit && product.conversionRate ? (
|
||||
<span className="text-sm text-gray-500">
|
||||
1 {product.largeUnit} = {Number(product.conversionRate)} {product.baseUnit || product.unit}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.lastPrice ? (
|
||||
<span>
|
||||
${product.lastPrice.toLocaleString()} / {product.baseUnit || product.unit || "單位"}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEdit(product)}
|
||||
className="button-outlined-primary"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onRemove(product)}
|
||||
className="button-outlined-error"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="h-10 px-3 py-2 bg-gray-50/50 border border-border rounded-md text-gray-600 font-medium text-sm flex items-center">
|
||||
{item.baseUnit || "-"}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs">$</span>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
value={item.lastPrice === undefined ? "" : item.lastPrice}
|
||||
onChange={(e) => onItemChange(index, "lastPrice", e.target.value === "" ? 0 : Number(e.target.value))}
|
||||
placeholder="0.00"
|
||||
className="pl-6 h-10 w-full text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-center">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-error h-10 w-10 p-0"
|
||||
title="移除項目"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要移除此商品嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
此動作將從待更新清單中移除「{item.productName || "此商品"}」。需點擊下方的「更新供貨商品」才會正式生效。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="button-outlined-primary">取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => onRemoveItem(index)}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
確定移除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
|
||||
241
resources/js/Pages/Vendor/Show.tsx
vendored
241
resources/js/Pages/Vendor/Show.tsx
vendored
@@ -1,29 +1,14 @@
|
||||
/**
|
||||
* 廠商詳細資訊頁面
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Head, Link, router } from "@inertiajs/react";
|
||||
import { Phone, Mail, Plus, ArrowLeft, Contact2 } from "lucide-react";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
import SupplyProductList from "@/Components/Vendor/SupplyProductList";
|
||||
import AddSupplyProductDialog from "@/Components/Vendor/AddSupplyProductDialog";
|
||||
import EditSupplyProductDialog from "@/Components/Vendor/EditSupplyProductDialog";
|
||||
import type { Vendor } from "@/Pages/Vendor/Index";
|
||||
import type { SupplyProduct } from "@/types/vendor";
|
||||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Pivot {
|
||||
last_price: number | null;
|
||||
@@ -33,12 +18,11 @@ interface VendorProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
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
|
||||
purchaseUnit?: string;
|
||||
purchase_unit?: string;
|
||||
conversion_rate?: number;
|
||||
pivot: Pivot;
|
||||
@@ -53,78 +37,102 @@ interface ShowProps {
|
||||
products: any[];
|
||||
}
|
||||
|
||||
export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState<SupplyProduct | null>(null);
|
||||
export default function VendorShow({ vendor, products: allProducts }: ShowProps) {
|
||||
const [items, setItems] = useState<SupplyProduct[]>([]);
|
||||
|
||||
// 轉換後端資料格式為前端組件需要的格式
|
||||
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;
|
||||
// 初始化資料
|
||||
useEffect(() => {
|
||||
const initialItems: SupplyProduct[] = vendor.products.map(p => {
|
||||
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), {
|
||||
product_id: productId,
|
||||
last_price: lastPrice,
|
||||
}, {
|
||||
onSuccess: () => setShowAddDialog(false),
|
||||
return {
|
||||
id: String(p.id) + "_" + Math.random().toString(36).substr(2, 9), // 加上隨機碼以確保 key 唯一
|
||||
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,
|
||||
};
|
||||
});
|
||||
setItems(initialItems);
|
||||
}, [vendor.products]);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const newItem: SupplyProduct = {
|
||||
id: "new_" + Math.random().toString(36).substr(2, 9),
|
||||
productId: "",
|
||||
productName: "",
|
||||
unit: "個",
|
||||
lastPrice: undefined,
|
||||
};
|
||||
setItems([...items, newItem]);
|
||||
};
|
||||
|
||||
const handleEditProduct = (product: SupplyProduct) => {
|
||||
setSelectedProduct(product);
|
||||
setShowEditDialog(true);
|
||||
const handleRemoveItem = (index: number) => {
|
||||
const newItems = [...items];
|
||||
newItems.splice(index, 1);
|
||||
setItems(newItems);
|
||||
};
|
||||
|
||||
const handleUpdateProduct = (productId: string, lastPrice?: number) => {
|
||||
router.put(route('vendors.products.update', [vendor.id, productId]), {
|
||||
last_price: lastPrice,
|
||||
const handleItemChange = (index: number, field: keyof SupplyProduct, value: any) => {
|
||||
const newItems = [...items];
|
||||
const item = { ...newItems[index] };
|
||||
|
||||
if (field === "productId") {
|
||||
const product = allProducts.find(p => String(p.id) === String(value));
|
||||
if (product) {
|
||||
item.productId = String(product.id);
|
||||
item.productName = product.name;
|
||||
item.baseUnit = product.baseUnit?.name || product.base_unit?.name || product.base_unit || "個";
|
||||
item.largeUnit = product.largeUnit?.name || product.large_unit?.name;
|
||||
item.conversionRate = product.conversion_rate;
|
||||
item.unit = item.baseUnit || "個";
|
||||
} else {
|
||||
item.productId = value;
|
||||
item.productName = "";
|
||||
}
|
||||
} else {
|
||||
(item as any)[field] = value;
|
||||
}
|
||||
|
||||
newItems[index] = item;
|
||||
setItems(newItems);
|
||||
};
|
||||
|
||||
const handleSaveAll = () => {
|
||||
// 過濾掉沒有選擇商品的項目
|
||||
const validItems = items.filter(item => item.productId !== "");
|
||||
|
||||
if (validItems.length === 0 && items.length > 0) {
|
||||
toast.error("請至少選擇一個有效的商品,或移除空白列");
|
||||
return;
|
||||
}
|
||||
|
||||
// 檢查重複商品
|
||||
const productIds = validItems.map(i => i.productId);
|
||||
if (new Set(productIds).size !== productIds.length) {
|
||||
toast.error("供貨清單中有重複的商品,請檢查");
|
||||
return;
|
||||
}
|
||||
|
||||
router.put(route('vendors.products.sync', vendor.id), {
|
||||
products: validItems.map(item => ({
|
||||
product_id: item.productId,
|
||||
last_price: item.lastPrice,
|
||||
}))
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setShowEditDialog(false);
|
||||
setSelectedProduct(null);
|
||||
toast.success("供貨商品已更新");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("更新失敗,請檢查欄位格式");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveProduct = (product: SupplyProduct) => {
|
||||
setSelectedProduct(product);
|
||||
setShowRemoveDialog(true);
|
||||
};
|
||||
|
||||
const handleConfirmRemove = () => {
|
||||
if (selectedProduct) {
|
||||
router.delete(route('vendors.products.destroy', [vendor.id, selectedProduct.productId]), {
|
||||
onSuccess: () => {
|
||||
setShowRemoveDialog(false);
|
||||
setSelectedProduct(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout breadcrumbs={getShowBreadcrumbs("vendors", `廠商詳情 (${vendor.name})`)}>
|
||||
<Head title={`廠商詳情 - ${vendor.name}`} />
|
||||
@@ -164,11 +172,11 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">廠商簡稱</Label>
|
||||
<p className="mt-1 font-medium">{vendor.short_name || "-"}</p>
|
||||
<p className="mt-1 font-medium">{vendor.shortName || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">統一編號</Label>
|
||||
<p className="mt-1">{vendor.tax_id || "-"}</p>
|
||||
<p className="mt-1">{vendor.taxId || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">負責人</Label>
|
||||
@@ -187,7 +195,7 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">聯絡人</Label>
|
||||
<p className="mt-1">{vendor.contact_name || "-"}</p>
|
||||
<p className="mt-1">{vendor.contactName || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">聯絡電話</Label>
|
||||
@@ -212,9 +220,9 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
{/* 供貨商品列表 */}
|
||||
<div className="bg-white rounded-lg border border-border p-6 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>供貨商品</h3>
|
||||
<h3 className="font-bold">供貨商品</h3>
|
||||
<Button
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
onClick={handleAddItem}
|
||||
className="gap-2 button-filled-primary"
|
||||
size="sm"
|
||||
>
|
||||
@@ -222,57 +230,30 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
新增供貨商品
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SupplyProductList
|
||||
products={supplyProducts}
|
||||
onEdit={handleEditProduct}
|
||||
onRemove={handleRemoveProduct}
|
||||
items={items}
|
||||
allProducts={allProducts}
|
||||
onRemoveItem={handleRemoveItem}
|
||||
onItemChange={handleItemChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 新增供貨商品對話框 */}
|
||||
<AddSupplyProductDialog
|
||||
open={showAddDialog}
|
||||
products={products}
|
||||
existingSupplyProducts={supplyProducts}
|
||||
onClose={() => setShowAddDialog(false)}
|
||||
onAdd={handleAddProduct}
|
||||
/>
|
||||
|
||||
{/* 編輯供貨商品對話框 */}
|
||||
<EditSupplyProductDialog
|
||||
open={showEditDialog}
|
||||
product={selectedProduct}
|
||||
onClose={() => {
|
||||
setShowEditDialog(false);
|
||||
setSelectedProduct(null);
|
||||
}}
|
||||
onSave={handleUpdateProduct}
|
||||
/>
|
||||
|
||||
{/* 取消供貨確認對話框 */}
|
||||
<AlertDialog open={showRemoveDialog} onOpenChange={setShowRemoveDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確認取消供貨</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
確定要將「{selectedProduct?.productName}」從供貨列表中移除嗎?此操作無法撤銷。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
className="gap-2 button-outlined-primary"
|
||||
>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="gap-2 button-filled-error"
|
||||
onClick={handleConfirmRemove}
|
||||
>
|
||||
確認移除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* 底部按鈕 - 移至容器外 */}
|
||||
<div className="mt-6 flex items-center justify-end gap-4 py-6 border-t border-gray-100">
|
||||
<Link href="/vendors">
|
||||
<Button variant="ghost" className="h-11 px-6 text-gray-500 hover:text-gray-700">
|
||||
取消
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
onClick={handleSaveAll}
|
||||
className="bg-primary hover:bg-primary/90 text-white shadow-primary/20 h-11 px-8 text-lg font-bold rounded-xl"
|
||||
size="lg"
|
||||
>
|
||||
更新供貨商品
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
|
||||
83
resources/views/docs/api.blade.php
Normal file
83
resources/views/docs/api.blade.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $title ?? 'API Documentation' }} - Star ERP</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
code, pre {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.prose pre {
|
||||
background-color: #1e293b;
|
||||
color: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
.sidebar-link.active {
|
||||
color: #0ea5e9;
|
||||
background-color: #f0f9ff;
|
||||
border-right: 4px solid #0ea5e9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-900">
|
||||
<div class="flex min-h-screen">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-white border-r border-slate-200 fixed h-full overflow-y-auto hidden md:block">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-2 mb-8">
|
||||
<div class="w-8 h-8 bg-sky-500 rounded-lg flex items-center justify-center text-white font-bold">S</div>
|
||||
<span class="font-bold text-xl tracking-tight">Star ERP</span>
|
||||
</div>
|
||||
|
||||
<nav class="space-y-1">
|
||||
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-3">文件導覽</p>
|
||||
@foreach($toc as $item)
|
||||
<a href="#{{ $item['id'] }}" class="block px-3 py-2 text-sm font-medium text-slate-600 hover:text-sky-600 hover:bg-slate-50 rounded-md transition-all duration-200">
|
||||
{{ $item['text'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 md:ml-64 p-6 md:p-12 lg:p-20">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<article class="prose prose-slate prose-sky max-w-none
|
||||
prose-headings:scroll-mt-20
|
||||
prose-h1:text-4xl prose-h1:font-extrabold prose-h1:tracking-tight
|
||||
prose-pre:p-6 prose-pre:text-sm
|
||||
prose-table:border prose-table:rounded-xl prose-table:overflow-hidden
|
||||
prose-th:bg-slate-100 prose-th:p-4
|
||||
prose-td:p-4 prose-td:border-t prose-td:border-slate-100">
|
||||
{!! $content !!}
|
||||
</article>
|
||||
|
||||
<footer class="mt-20 pt-8 border-t border-slate-200 text-slate-400 text-sm flex justify-between">
|
||||
<span>© {{ date('Y') }} Star ERP System. All rights reserved.</span>
|
||||
<span>整合介面版本 v1.2</span>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="md:hidden fixed bottom-6 right-6">
|
||||
<button id="menu-toggle" class="w-14 h-14 bg-sky-500 text-white rounded-full shadow-lg flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user