Files
star-erp/resources/js/Pages/Vendor/Show.tsx

280 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 廠商詳細資訊頁面
*/
import { useState } 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";
interface Pivot {
last_price: number | null;
}
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
purchase_unit?: string;
conversion_rate?: number;
pivot: Pivot;
}
interface ExtendedVendor extends Vendor {
products: VendorProduct[];
}
interface ShowProps {
vendor: ExtendedVendor;
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);
// 轉換後端資料格式為前端組件需要的格式
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), {
product_id: productId,
last_price: lastPrice,
}, {
onSuccess: () => setShowAddDialog(false),
});
};
const handleEditProduct = (product: SupplyProduct) => {
setSelectedProduct(product);
setShowEditDialog(true);
};
const handleUpdateProduct = (productId: string, lastPrice?: number) => {
router.put(route('vendors.products.update', [vendor.id, productId]), {
last_price: lastPrice,
}, {
onSuccess: () => {
setShowEditDialog(false);
setSelectedProduct(null);
}
});
};
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}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* 返回按鈕 */}
<div className="mb-6">
<Link href="/vendors">
<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">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Contact2 className="h-6 w-6 text-[#01ab83]" />
</h1>
<p className="text-gray-500 mt-1"></p>
</div>
</div>
</div>
{/* 基本資料 */}
<div className="bg-white rounded-lg border border-border p-6 mb-6 shadow-sm">
<h3 className="mb-4 text-primary font-bold"></h3>
<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 font-medium flex items-baseline gap-2">
{vendor.name}
<span className="text-sm text-muted-foreground">({vendor.code})</span>
</p>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<p className="mt-1 font-medium">{vendor.short_name || "-"}</p>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<p className="mt-1">{vendor.tax_id || "-"}</p>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<p className="mt-1">{vendor.owner || "-"}</p>
</div>
<div className="md:col-span-2">
<Label className="text-muted-foreground text-xs"></Label>
<p className="mt-1 whitespace-pre-wrap">{vendor.remark || "-"}</p>
</div>
</div>
</div>
{/* 聯絡資料 */}
<div className="bg-white rounded-lg border border-border p-6 mb-6 shadow-sm">
<h3 className="mb-4 text-primary font-bold"></h3>
<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>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<p className="mt-1 flex items-center gap-2">
<Phone className="h-4 w-4 text-muted-foreground" />
{vendor.phone || vendor.tel || "-"}
</p>
</div>
<div>
<Label className="text-muted-foreground text-xs">Email</Label>
<p className="mt-1 flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
{vendor.email || "-"}</p>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<p className="mt-1">{vendor.address || "-"}</p>
</div>
</div>
</div>
{/* 供貨商品列表 */}
<div className="bg-white rounded-lg border border-border p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3></h3>
<Button
onClick={() => setShowAddDialog(true)}
className="gap-2 button-filled-primary"
size="sm"
>
<Plus className="h-4 w-4" />
</Button>
</div>
<SupplyProductList
products={supplyProducts}
onEdit={handleEditProduct}
onRemove={handleRemoveProduct}
/>
</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>
</AuthenticatedLayout>
);
}