first commit

This commit is contained in:
2025-12-30 15:03:19 +08:00
commit c735c36009
902 changed files with 83591 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
/**
* 新增供貨商品對話框
*/
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 => sp.productId));
return products.filter(p => !existingIds.has(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" 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.purchase_unit || 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.purchase_unit || selectedProduct.base_unit || "個") : "-"}
</div>
</div>
{/* 上次採購價格 */}
<div>
<Label className="text-muted-foreground text-xs"></Label>
<Input
type="number"
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>
);
}

View File

@@ -0,0 +1,117 @@
/**
* 編輯供貨商品對話框
*/
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"></Label>
<Input
type="number"
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>
);
}

View File

@@ -0,0 +1,81 @@
import { Pencil, Trash2 } from "lucide-react";
import { Button } from "@/Components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import type { SupplyProduct } from "@/types/vendor";
interface SupplyProductListProps {
products: SupplyProduct[];
onEdit: (product: SupplyProduct) => void;
onRemove: (product: SupplyProduct) => void;
}
export default function SupplyProductList({
products,
onEdit,
onRemove,
}: SupplyProductListProps) {
return (
<div className="bg-white rounded-lg border shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead className="font-semibold"></TableHead>
<TableHead className="font-semibold"></TableHead>
<TableHead className="text-right font-semibold"></TableHead>
<TableHead className="text-center font-semibold w-[150px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
products.map((product, index) => (
<TableRow key={product.id}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell>{product.productName}</TableCell>
<TableCell>{product.unit}</TableCell>
<TableCell className="text-right">
{product.lastPrice ? `$${product.lastPrice.toLocaleString()}` : "-"}
</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>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,55 @@
/**
* 廠商刪除確認對話框
*/
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/Components/ui/alert-dialog";
import type { Supplier } from "@/types/vendor";
interface VendorDeleteDialogProps {
open: boolean;
supplier: Supplier | null;
onConfirm: () => void;
onCancel: () => void;
}
export default function VendorDeleteDialog({
open,
supplier,
onConfirm,
onCancel,
}: VendorDeleteDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onCancel}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{supplier?.name}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
className="gap-2 button-outlined-primary"
>
</AlertDialogCancel>
<AlertDialogAction
className="gap-2 button-filled-error"
onClick={onConfirm}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -0,0 +1,241 @@
import { useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import { useForm } from "@inertiajs/react";
import { toast } from "sonner";
import type { Vendor } from "@/Pages/Vendor/Index";
interface VendorDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
vendor: Vendor | null;
}
export default function VendorDialog({
open,
onOpenChange,
vendor,
}: VendorDialogProps) {
const { data, setData, post, put, processing, errors, reset, clearErrors } = useForm({
name: "",
short_name: "",
tax_id: "",
owner: "",
contact_name: "",
tel: "",
phone: "",
email: "",
address: "",
remark: "",
});
useEffect(() => {
if (open) {
clearErrors();
if (vendor) {
setData({
name: vendor.name,
short_name: vendor.short_name || "",
tax_id: vendor.tax_id || "",
owner: vendor.owner || "",
contact_name: vendor.contact_name || "",
tel: vendor.tel || "",
phone: vendor.phone || "",
email: vendor.email || "",
address: vendor.address || "",
remark: vendor.remark || "",
});
} else {
reset();
}
}
}, [open, vendor]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (vendor) {
put(route("vendors.update", vendor.id), {
onSuccess: () => {
toast.success("廠商資料已更新");
onOpenChange(false);
reset();
},
onError: () => {
toast.error("更新失敗,請檢查輸入資料");
}
});
} else {
post(route("vendors.store"), {
onSuccess: () => {
toast.success("廠商已新增");
onOpenChange(false);
reset();
},
onError: () => {
toast.error("新增失敗,請檢查輸入資料");
}
});
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{vendor ? "編輯廠商" : "新增廠商"}</DialogTitle>
<DialogDescription>
{vendor ? "修改廠商基本資料與聯絡資訊" : "建立新的廠商資料"}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6 py-4">
{/* 基本資訊區塊 */}
<div className="space-y-4">
<h3 className="text-lg font-medium border-b pb-2 text-primary"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={data.name}
onChange={(e) => setData("name", e.target.value)}
placeholder="例:宏達食品有限公司"
className={errors.name ? "border-red-500" : ""}
/>
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="short_name"></Label>
<Input
id="short_name"
value={data.short_name}
onChange={(e) => setData("short_name", e.target.value)}
placeholder="例:宏達食品"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tax_id"></Label>
<Input
id="tax_id"
value={data.tax_id}
onChange={(e) => setData("tax_id", e.target.value)}
placeholder="8 位數字"
maxLength={8}
/>
{errors.tax_id && <p className="text-sm text-red-500">{errors.tax_id}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="owner"></Label>
<Input
id="owner"
value={data.owner}
onChange={(e) => setData("owner", e.target.value)}
placeholder="負責人姓名"
/>
</div>
</div>
</div>
{/* 聯絡資訊區塊 */}
<div className="space-y-4">
<h3 className="text-lg font-medium border-b pb-2 text-primary"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="contact_name"></Label>
<Input
id="contact_name"
value={data.contact_name}
onChange={(e) => setData("contact_name", e.target.value)}
placeholder="業務或聯絡窗口姓名"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={data.email}
onChange={(e) => setData("email", e.target.value)}
placeholder="example@mail.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tel"></Label>
<Input
id="tel"
value={data.tel}
onChange={(e) => setData("tel", e.target.value)}
placeholder="例02-23456789"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={data.phone}
onChange={(e) => setData("phone", e.target.value)}
placeholder="例0912-345-678"
/>
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="address"></Label>
<Input
id="address"
value={data.address}
onChange={(e) => setData("address", e.target.value)}
placeholder="完整的營業或通訊地址"
/>
</div>
</div>
</div>
{/* 備註區塊 */}
<div className="space-y-2">
<Label htmlFor="remark"></Label>
<Textarea
id="remark"
value={data.remark}
onChange={(e) => setData("remark", e.target.value)}
placeholder="特殊交易習慣、配送時間要求等..."
className="resize-none"
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="button-outlined-primary"
>
</Button>
<Button type="submit" className="button-filled-primary" disabled={processing}>
{processing ? "儲存... " : (vendor ? "儲存變更" : "新增廠商")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,140 @@
/**
* 廠商新增/編輯表單對話框
*/
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 { Supplier, SupplierFormData } from "@/types/vendor";
interface VendorFormDialogProps {
open: boolean;
supplier: Supplier | null;
onClose: () => void;
onSave: (data: SupplierFormData) => void;
}
const initialFormData: SupplierFormData = {
name: "",
contact: "",
phone: "",
email: "",
};
export default function VendorFormDialog({
open,
supplier,
onClose,
onSave,
}: VendorFormDialogProps) {
const [formData, setFormData] = useState<SupplierFormData>(initialFormData);
const isEditing = !!supplier;
useEffect(() => {
if (supplier) {
setFormData({
name: supplier.name,
contact: supplier.contact || "",
phone: supplier.phone || "",
email: supplier.email || "",
});
} else {
setFormData(initialFormData);
}
}, [supplier, open]);
const handleSave = () => {
onSave(formData);
setFormData(initialFormData);
};
const handleCancel = () => {
onClose();
setFormData(initialFormData);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{isEditing ? "編輯廠商" : "新增廠商"}</DialogTitle>
<DialogDescription>
{isEditing ? "編輯供應商的詳細資料。" : "新增供應商的詳細資料。"}
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
<div>
<h3 className="mb-4"></h3>
<div className="space-y-4">
<div>
<Label className="text-muted-foreground text-xs"></Label>
<Input
placeholder="輸入廠商名稱"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<Input
placeholder="輸入聯絡人姓名"
value={formData.contact}
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label className="text-muted-foreground text-xs"></Label>
<Input
placeholder="例02-1234-5678"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label className="text-muted-foreground text-xs">Email</Label>
<Input
type="email"
placeholder="例vendor@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="mt-1"
/>
</div>
</div>
</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>
);
}

View File

@@ -0,0 +1,130 @@
/**
* 廠商列表顯示元件
*/
import { Phone, Mail, Package, Pencil, Trash2, Calendar, Eye } from "lucide-react";
import { Button } from "@/Components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import type { Supplier } from "@/types/vendor";
interface VendorListProps {
suppliers: Supplier[];
searchQuery: string;
onViewDetails: (supplier: Supplier) => void;
onEdit: (supplier: Supplier) => void;
onDelete: (supplier: Supplier) => void;
}
export default function VendorList({
suppliers,
searchQuery,
onViewDetails,
onEdit,
onDelete,
}: VendorListProps) {
const isEmpty = suppliers.length === 0;
const emptyMessage = searchQuery ? "未找到符合條件的廠商" : "尚無廠商資料";
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleDateString("zh-TW", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
};
return (
<div className="bg-card rounded-lg border border-border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[140px]"></TableHead>
<TableHead className="w-[180px]">Email</TableHead>
<TableHead className="w-[130px]"></TableHead>
<TableHead className="w-[220px] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isEmpty ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
{emptyMessage}
</TableCell>
</TableRow>
) : (
suppliers.map((supplier) => (
<TableRow key={supplier.id}>
<TableCell>
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground" />
<span>{supplier.name}</span>
</div>
</TableCell>
<TableCell>{supplier.contact || "-"}</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-sm">
<Phone className="h-3 w-3 text-muted-foreground" />
{supplier.phone || "-"}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-sm">
<Mail className="h-3 w-3 text-muted-foreground" />
{supplier.email || "-"}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-3 w-3 text-muted-foreground" />
{formatDate(supplier.lastPurchaseDate)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onViewDetails(supplier)}
className="gap-1 button-outlined-primary"
>
<Eye className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onEdit(supplier)}
className="gap-1 button-outlined-primary"
>
<Pencil className="h-3 w-3" />
</Button>
<Button
size="sm"
onClick={() => onDelete(supplier)}
className="gap-1 button-filled-error"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,37 @@
/**
* 廠商搜尋列元件
*/
import { Search, Plus } from "lucide-react";
import { Input } from "@/Components/ui/input";
import { Button } from "@/Components/ui/button";
interface VendorSearchBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
onAddClick: () => void;
}
export default function VendorSearchBar({
searchQuery,
onSearchChange,
onAddClick,
}: VendorSearchBarProps) {
return (
<div className="flex justify-between items-center">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="搜尋廠商名稱 / 聯絡人 / Email"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9 input-outlined-primary"
/>
</div>
<Button onClick={onAddClick} className="gap-2 button-filled-primary">
<Plus className="h-4 w-4" />
</Button>
</div>
);
}

View File

@@ -0,0 +1,166 @@
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { Button } from "@/Components/ui/button";
import { Pencil, Trash2, Eye, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/Components/ui/alert-dialog";
import type { Vendor } from "@/Pages/Vendor/Index";
interface VendorTableProps {
vendors: Vendor[];
onView: (vendor: Vendor) => void;
onEdit: (vendor: Vendor) => void;
onDelete: (id: number) => void;
sortField: string | null;
sortDirection: "asc" | "desc" | null;
onSort: (field: string) => void;
}
export default function VendorTable({
vendors,
onView,
onEdit,
onDelete,
sortField,
sortDirection,
onSort,
}: VendorTableProps) {
const SortIcon = ({ field }: { field: string }) => {
if (sortField !== field) {
return <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
}
if (sortDirection === "asc") {
return <ArrowUp className="h-4 w-4 text-primary ml-1" />;
}
if (sortDirection === "desc") {
return <ArrowDown className="h-4 w-4 text-primary ml-1" />;
}
return <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
};
return (
<div className="bg-white rounded-lg shadow-sm border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead>
<button onClick={() => onSort("code")} className="flex items-center hover:text-gray-900 font-semibold">
<SortIcon field="code" />
</button>
</TableHead>
<TableHead>
<button onClick={() => onSort("name")} className="flex items-center hover:text-gray-900 font-semibold">
<SortIcon field="name" />
</button>
</TableHead>
<TableHead>
<button onClick={() => onSort("owner")} className="flex items-center hover:text-gray-900 font-semibold">
<SortIcon field="owner" />
</button>
</TableHead>
<TableHead>
<button onClick={() => onSort("contact_name")} className="flex items-center hover:text-gray-900 font-semibold">
<SortIcon field="contact_name" />
</button>
</TableHead>
<TableHead>
<button onClick={() => onSort("phone")} className="flex items-center hover:text-gray-900 font-semibold">
<SortIcon field="phone" />
</button>
</TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vendors.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-gray-500">
</TableCell>
</TableRow>
) : (
vendors.map((vendor, index) => (
<TableRow key={vendor.id}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell className="font-mono text-sm text-gray-700">
{vendor.code}
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{vendor.name}</span>
{vendor.short_name && <span className="text-xs text-gray-400">{vendor.short_name}</span>}
</div>
</TableCell>
<TableCell>{vendor.owner || '-'}</TableCell>
<TableCell>{vendor.contact_name || '-'}</TableCell>
<TableCell>{vendor.phone || vendor.tel || '-'}</TableCell>
<TableCell className="text-center">
<div className="flex justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onView(vendor)}
className="button-outlined-primary"
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onEdit(vendor)}
className="button-outlined-primary"
>
<Pencil className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" className="button-outlined-error">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{vendor.name}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => onDelete(vendor.id)}
className="bg-red-600 hover:bg-red-700"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
}