diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-02 08:44:17 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-02 08:44:17 +0000 |
| commit | 8da223a416ec7d2be5743f312ed1d8c6d64949e2 (patch) | |
| tree | 6333679b326e3508913774f3d5afaabca1f4f198 /lib/material | |
| parent | 6eb06a925811cfefb34b6c286f6bdfe2f214ac2b (diff) | |
(김준회) 협력업체 관리 메뉴에서, 공급품목(패키지) 제거, MDG 자재마스터 기반 벤더별 공급품목 메뉴 구현 (정의서+강미경프로 요구대로 구현)
Diffstat (limited to 'lib/material')
7 files changed, 937 insertions, 0 deletions
diff --git a/lib/material/vendor-material/add-confirmed-material.tsx b/lib/material/vendor-material/add-confirmed-material.tsx new file mode 100644 index 00000000..bc232a1b --- /dev/null +++ b/lib/material/vendor-material/add-confirmed-material.tsx @@ -0,0 +1,183 @@ +"use client"; + +import * as React from "react"; +import { useState } from "react"; +import { useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; +import { Plus, Loader2 } from "lucide-react"; +import { MaterialSelector } from "@/components/common/material/material-selector"; +import { MaterialSearchItem } from "@/lib/material/material-group-service"; +import { addConfirmedMaterial, VendorPossibleMaterial } from "../vendor-possible-material-service"; + +interface AddConfirmedMaterialProps { + vendorId: number; + existingConfirmedMaterials: VendorPossibleMaterial[]; + onMaterialAdded?: () => void; +} + +export function AddConfirmedMaterial({ + vendorId, + existingConfirmedMaterials, + onMaterialAdded, +}: AddConfirmedMaterialProps) { + const { data: session } = useSession(); + const [open, setOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [selectedMaterials, setSelectedMaterials] = useState<MaterialSearchItem[]>([]); + + // 이미 등록된 자재그룹코드들의 Set + const existingMaterialCodes = new Set( + existingConfirmedMaterials.map(material => material.itemCode).filter(Boolean) + ); + + // 자재 선택 시 중복 체크 + const handleMaterialsChange = (materials: MaterialSearchItem[]) => { + // 이미 등록된 자재가 있는지 확인 + const duplicatedMaterials = materials.filter(material => + existingMaterialCodes.has(material.materialGroupCode) + ); + + if (duplicatedMaterials.length > 0) { + const duplicatedCodes = duplicatedMaterials.map(m => m.materialGroupCode).join(', '); + toast.error(`이미 등록된 자재그룹코드입니다: ${duplicatedCodes}`); + + // 중복되지 않은 자재만 선택 + const validMaterials = materials.filter(material => + !existingMaterialCodes.has(material.materialGroupCode) + ); + setSelectedMaterials(validMaterials); + } else { + setSelectedMaterials(materials); + } + }; + + const handleSubmit = async () => { + if (!session?.user) { + toast.error("로그인이 필요합니다."); + return; + } + + if (selectedMaterials.length === 0) { + toast.error("추가할 자재를 선택해주세요."); + return; + } + + setIsLoading(true); + + try { + // 선택된 자재들을 각각 추가 + for (const material of selectedMaterials) { + const materialData = { + itemCode: material.materialGroupCode, + itemName: material.materialName, + }; + + await addConfirmedMaterial( + vendorId, + materialData, + Number(session.user.id), + session.user.name || "알 수 없음" + ); + } + + toast.success(`${selectedMaterials.length}개의 자재가 확정정보에 추가되었습니다.`); + + // 폼 리셋 + setSelectedMaterials([]); + setOpen(false); + + // 부모 컴포넌트에 추가 완료 알림 + onMaterialAdded?.(); + + } catch (error) { + console.error("자재 추가 실패:", error); + + // 에러 메시지를 더 구체적으로 표시 + if (error instanceof Error) { + if (error.message.includes("이미 확정정보에 등록되어 있습니다")) { + toast.error(error.message); + } else { + toast.error(`자재 추가 실패: ${error.message}`); + } + } else { + toast.error("자재 추가 중 알 수 없는 오류가 발생했습니다."); + } + } finally { + setIsLoading(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + <Button size="sm" className="gap-2"> + <Plus className="h-4 w-4" /> + 추가 등록 + </Button> + </DialogTrigger> + <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>확정정보 자재 추가</DialogTitle> + <DialogDescription> + 구매담당자 권한으로 확정 공급품목을 추가합니다. 상세 정보는 I/F를 통해 업데이트됩니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 자재 선택 */} + <div className="space-y-2"> + <Label>자재 선택 *</Label> + <MaterialSelector + selectedMaterials={selectedMaterials} + onMaterialsChange={handleMaterialsChange} + singleSelect={false} + placeholder="자재그룹코드 또는 자재명으로 검색..." + noValuePlaceHolder="자재를 선택해주세요" + maxSelections={10} + closeOnSelect={false} + excludeMaterialCodes={existingMaterialCodes} + /> + {existingMaterialCodes.size > 0 && ( + <p className="text-xs text-muted-foreground"> + 💡 이미 등록된 자재그룹코드는 자동으로 제외됩니다. + </p> + )} + <p className="text-sm text-muted-foreground"> + 최대 10개까지 선택 가능합니다. 등록자는 현재 로그인한 사용자로 자동 설정됩니다. + </p> + </div> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="button" + onClick={handleSubmit} + disabled={isLoading || selectedMaterials.length === 0} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {isLoading ? "추가 중..." : "추가"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/lib/material/vendor-material/columns.tsx b/lib/material/vendor-material/columns.tsx new file mode 100644 index 00000000..8f706d63 --- /dev/null +++ b/lib/material/vendor-material/columns.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; +import { VendorPossibleMaterial } from "../vendor-possible-material-service"; +import { formatDate } from "@/lib/utils"; + +// 확정정보 테이블 컬럼 +export const confirmedMaterialsColumns: ColumnDef<VendorPossibleMaterial>[] = [ + { + accessorKey: "vendorTypeNameEn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="업체유형" /> + ), + cell: ({ row }) => { + const vendorTypeNameEn = row.getValue("vendorTypeNameEn") as string; + return <span>{vendorTypeNameEn || "-"}</span>; + }, + }, + { + accessorKey: "itemCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹" /> + ), + cell: ({ row }) => { + const itemCode = row.getValue("itemCode") as string; + return <span className="font-mono">{itemCode || "-"}</span>; + }, + }, + { + accessorKey: "itemName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹명" /> + ), + cell: ({ row }) => { + const itemName = row.getValue("itemName") as string; + return <span>{itemName || "-"}</span>; + }, + }, + { + accessorKey: "recentPoNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최근 Po No." /> + ), + cell: ({ row }) => { + const recentPoNo = row.getValue("recentPoNo") as string; + return <span className="font-mono">{recentPoNo || "-"}</span>; + }, + }, + { + accessorKey: "recentPoDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최근 Po일" /> + ), + cell: ({ row }) => { + const date = row.getValue("recentPoDate") as Date; + return <span>{date ? formatDate(date) : "-"}</span>; + }, + }, + { + accessorKey: "recentDeliveryDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최근 납품일" /> + ), + cell: ({ row }) => { + const date = row.getValue("recentDeliveryDate") as Date; + return <span>{date ? formatDate(date) : "-"}</span>; + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록일" /> + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return <span>{formatDate(date)}</span>; + }, + }, + { + accessorKey: "registerUserName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록자명" /> + ), + cell: ({ row }) => { + const userName = row.getValue("registerUserName") as string; + return <span>{userName || "-"}</span>; + }, + }, + { + accessorKey: "recentOrderDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최근발주일" /> + ), + cell: ({ row }) => { + const date = row.getValue("recentOrderDate") as Date; + return <span>{date ? formatDate(date) : "-"}</span>; + }, + }, +]; + +// 업체입력정보 테이블 컬럼 +export const vendorInputMaterialsColumns: ColumnDef<VendorPossibleMaterial>[] = [ + { + accessorKey: "vendorTypeNameEn", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="업체유형" /> + ), + cell: ({ row }) => { + const vendorTypeNameEn = row.getValue("vendorTypeNameEn") as string; + return <span>{vendorTypeNameEn || "-"}</span>; + }, + }, + { + accessorKey: "itemCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹" /> + ), + cell: ({ row }) => { + const itemCode = row.getValue("itemCode") as string; + return <span className="font-mono">{itemCode || "-"}</span>; + }, + }, + { + accessorKey: "itemName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹명" /> + ), + cell: ({ row }) => { + const itemName = row.getValue("itemName") as string; + return <span>{itemName || "-"}</span>; + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록일" /> + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return <span>{formatDate(date)}</span>; + }, + }, + { + accessorKey: "registerUserName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록자명" /> + ), + cell: ({ row }) => { + const userName = row.getValue("registerUserName") as string; + return <span>{userName || "-"}</span>; + }, + }, +]; diff --git a/lib/material/vendor-material/confirmed-materials-table.tsx b/lib/material/vendor-material/confirmed-materials-table.tsx new file mode 100644 index 00000000..11282a24 --- /dev/null +++ b/lib/material/vendor-material/confirmed-materials-table.tsx @@ -0,0 +1,77 @@ +"use client"; + +import * as React from "react"; +import { DataTable } from "@/components/data-table/data-table"; +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { confirmedMaterialsColumns } from "./columns"; +import { useDataTable } from "@/hooks/use-data-table"; +import { getConfirmedMaterials, VendorPossibleMaterial } from "../vendor-possible-material-service"; + +interface ConfirmedMaterialsTableProps { + vendorId: number; + data: VendorPossibleMaterial[]; + pageCount: number; +} + +export function ConfirmedMaterialsTable({ + vendorId, + data, + pageCount, +}: ConfirmedMaterialsTableProps) { + const { table } = useDataTable({ + data, + columns: confirmedMaterialsColumns, + pageCount, + filterFields: [ + { + id: "itemCode", + label: "자재그룹", + placeholder: "자재그룹 검색...", + }, + { + id: "itemName", + label: "자재그룹명", + placeholder: "자재그룹명 검색...", + }, + ], + }); + + const handleAddMaterial = () => { + // TODO: 확정정보 추가 다이얼로그 열기 + console.log("확정정보 추가 버튼 클릭"); + }; + + return ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-semibold">확정정보</h3> + <Button onClick={handleAddMaterial} className="gap-2"> + <Plus className="h-4 w-4" /> + 추가등록 + </Button> + </div> + + <DataTable table={table}> + <DataTableToolbar + table={table} + filterFields={[ + { + id: "itemCode", + label: "자재그룹", + placeholder: "자재그룹 검색...", + }, + { + id: "itemName", + label: "자재그룹명", + placeholder: "자재그룹명 검색...", + }, + ]} + > + {/* 컬럼 선택 기능 제거 - DataTableViewOptions 없음 */} + </DataTableToolbar> + </DataTable> + </div> + ); +} diff --git a/lib/material/vendor-material/simple-vendor-materials-wrapper.tsx b/lib/material/vendor-material/simple-vendor-materials-wrapper.tsx new file mode 100644 index 00000000..7c0c7068 --- /dev/null +++ b/lib/material/vendor-material/simple-vendor-materials-wrapper.tsx @@ -0,0 +1,48 @@ +"use client"; + +import * as React from "react"; +import { useState, useCallback } from "react"; +import { SimpleVendorMaterials } from "./simple-vendor-materials"; +import { VendorPossibleMaterial, getAllConfirmedMaterials, getAllVendorInputMaterials } from "../vendor-possible-material-service"; + +interface SimpleVendorMaterialsWrapperProps { + vendorId: number; + initialConfirmedMaterials: VendorPossibleMaterial[]; + initialVendorInputMaterials: VendorPossibleMaterial[]; +} + +export function SimpleVendorMaterialsWrapper({ + vendorId, + initialConfirmedMaterials, + initialVendorInputMaterials, +}: SimpleVendorMaterialsWrapperProps) { + const [confirmedMaterials, setConfirmedMaterials] = useState(initialConfirmedMaterials); + const [vendorInputMaterials, setVendorInputMaterials] = useState(initialVendorInputMaterials); + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleDataRefresh = useCallback(async () => { + setIsRefreshing(true); + try { + const [newConfirmedMaterials, newVendorInputMaterials] = await Promise.all([ + getAllConfirmedMaterials(vendorId), + getAllVendorInputMaterials(vendorId) + ]); + + setConfirmedMaterials(newConfirmedMaterials); + setVendorInputMaterials(newVendorInputMaterials); + } catch (error) { + console.error("데이터 새로고침 실패:", error); + } finally { + setIsRefreshing(false); + } + }, [vendorId]); + + return ( + <SimpleVendorMaterials + vendorId={vendorId} + confirmedMaterials={confirmedMaterials} + vendorInputMaterials={vendorInputMaterials} + onDataRefresh={handleDataRefresh} + /> + ); +} diff --git a/lib/material/vendor-material/simple-vendor-materials.tsx b/lib/material/vendor-material/simple-vendor-materials.tsx new file mode 100644 index 00000000..73fdfd18 --- /dev/null +++ b/lib/material/vendor-material/simple-vendor-materials.tsx @@ -0,0 +1,372 @@ +"use client"; + +import * as React from "react"; +import { useState, useMemo } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { ArrowUpDown, Search } from "lucide-react"; +import { VendorPossibleMaterial } from "../vendor-possible-material-service"; +import { AddConfirmedMaterial } from "./add-confirmed-material"; +// formatDate 함수 제거 - YYYY-MM-DD 형식으로 직접 포맷 + +interface SimpleVendorMaterialsProps { + vendorId: number; + confirmedMaterials: VendorPossibleMaterial[]; + vendorInputMaterials: VendorPossibleMaterial[]; + onDataRefresh?: () => void; +} + +type SortField = "itemCode" | "itemName" | "createdAt" | "recentPoDate" | "recentDeliveryDate" | "vendorTypeNameEn" | "recentPoNo" | "registerUserName"; +type SortDirection = "asc" | "desc"; + +interface SortState { + field: SortField | null; + direction: SortDirection; +} + +export function SimpleVendorMaterials({ + vendorId, + confirmedMaterials, + vendorInputMaterials, + onDataRefresh, +}: SimpleVendorMaterialsProps) { + // 날짜를 YYYY-MM-DD 형식으로 포맷하는 함수 + const formatDateToYMD = (date: Date | null | undefined): string => { + if (!date) return "-"; + return date.toISOString().split('T')[0]; + }; + const [confirmedSearch, setConfirmedSearch] = useState(""); + const [vendorInputSearch, setVendorInputSearch] = useState(""); + const [confirmedSort, setConfirmedSort] = useState<SortState>({ field: null, direction: "asc" }); + const [vendorInputSort, setVendorInputSort] = useState<SortState>({ field: null, direction: "asc" }); + + // 검색 필터링 함수 + const filterMaterials = (materials: VendorPossibleMaterial[], searchTerm: string) => { + if (!searchTerm.trim()) return materials; + + const lowercaseSearch = searchTerm.toLowerCase(); + return materials.filter(material => + (material.itemCode?.toLowerCase().includes(lowercaseSearch)) || + (material.itemName?.toLowerCase().includes(lowercaseSearch)) + ); + }; + + // 정렬 함수 + const sortMaterials = (materials: VendorPossibleMaterial[], sortState: SortState) => { + if (!sortState.field) return materials; + + return [...materials].sort((a, b) => { + let aValue: string | Date; + let bValue: string | Date; + + switch (sortState.field) { + case "itemCode": + aValue = a.itemCode || ""; + bValue = b.itemCode || ""; + break; + case "itemName": + aValue = a.itemName || ""; + bValue = b.itemName || ""; + break; + case "createdAt": + aValue = a.createdAt; + bValue = b.createdAt; + break; + case "recentPoDate": + aValue = a.recentPoDate || new Date(0); + bValue = b.recentPoDate || new Date(0); + break; + case "recentDeliveryDate": + aValue = a.recentDeliveryDate || new Date(0); + bValue = b.recentDeliveryDate || new Date(0); + break; + case "vendorTypeNameEn": + aValue = a.vendorTypeNameEn || ""; + bValue = b.vendorTypeNameEn || ""; + break; + case "recentPoNo": + aValue = a.recentPoNo || ""; + bValue = b.recentPoNo || ""; + break; + case "registerUserName": + aValue = a.registerUserName || ""; + bValue = b.registerUserName || ""; + break; + default: + return 0; + } + + if (aValue < bValue) return sortState.direction === "asc" ? -1 : 1; + if (aValue > bValue) return sortState.direction === "asc" ? 1 : -1; + return 0; + }); + }; + + // 필터링 및 정렬된 데이터 + const filteredAndSortedConfirmed = useMemo(() => { + const filtered = filterMaterials(confirmedMaterials, confirmedSearch); + return sortMaterials(filtered, confirmedSort); + }, [confirmedMaterials, confirmedSearch, confirmedSort]); + + const filteredAndSortedVendorInput = useMemo(() => { + const filtered = filterMaterials(vendorInputMaterials, vendorInputSearch); + return sortMaterials(filtered, vendorInputSort); + }, [vendorInputMaterials, vendorInputSearch, vendorInputSort]); + + // 정렬 핸들러 + const handleSort = ( + field: SortField, + currentSort: SortState, + setSort: React.Dispatch<React.SetStateAction<SortState>> + ) => { + setSort(prev => ({ + field, + direction: prev.field === field && prev.direction === "asc" ? "desc" : "asc" + })); + }; + + // 정렬 버튼 컴포넌트 + const SortButton = ({ + field, + children, + onSort + }: { + field: SortField; + children: React.ReactNode; + onSort: (field: SortField) => void; + }) => ( + <Button + variant="ghost" + size="sm" + className="h-8 px-2" + onClick={() => onSort(field)} + > + {children} + <ArrowUpDown className="ml-2 h-4 w-4" /> + </Button> + ); + + return ( + <div className="space-y-8"> + {/* 확정정보 테이블 */} + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <div> + <CardTitle>확정정보</CardTitle> + <CardDescription> + 구매담당자가 확정한 공급 품목 정보입니다. + </CardDescription> + </div> + <AddConfirmedMaterial + vendorId={vendorId} + existingConfirmedMaterials={confirmedMaterials} + onMaterialAdded={onDataRefresh} + /> + </div> + <div className="flex items-center space-x-2"> + <Search className="h-4 w-4 text-muted-foreground" /> + <Input + placeholder="자재그룹코드 또는 자재그룹명으로 검색..." + value={confirmedSearch} + onChange={(e) => setConfirmedSearch(e.target.value)} + className="max-w-sm" + /> + </div> + </CardHeader> + <CardContent> + <div className="rounded-md border max-h-96 overflow-auto"> + <Table> + <TableHeader className="sticky top-0 bg-background z-10"> + <TableRow> + <TableHead> + <SortButton + field="vendorTypeNameEn" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 업체유형 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="itemCode" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 자재그룹코드 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="itemName" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 자재그룹명 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="recentPoNo" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 최근 Po No. + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="recentPoDate" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 최근 Po일 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="recentDeliveryDate" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 최근 납품일 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="createdAt" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 등록일 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="registerUserName" + onSort={(field) => handleSort(field, confirmedSort, setConfirmedSort)} + > + 등록자명 + </SortButton> + </TableHead> + </TableRow> + </TableHeader> + <TableBody> + {filteredAndSortedConfirmed.length === 0 ? ( + <TableRow> + <TableCell colSpan={8} className="text-center text-muted-foreground"> + {confirmedSearch ? "검색 결과가 없습니다." : "확정된 공급품목이 없습니다."} + </TableCell> + </TableRow> + ) : ( + filteredAndSortedConfirmed.map((material) => ( + <TableRow key={material.id}> + <TableCell>{material.vendorTypeNameEn || "-"}</TableCell> + <TableCell className="font-mono">{material.itemCode || "-"}</TableCell> + <TableCell>{material.itemName || "-"}</TableCell> + <TableCell className="font-mono">{material.recentPoNo || "-"}</TableCell> + <TableCell>{formatDateToYMD(material.recentPoDate)}</TableCell> + <TableCell>{formatDateToYMD(material.recentDeliveryDate)}</TableCell> + <TableCell>{formatDateToYMD(material.createdAt)}</TableCell> + <TableCell>{material.registerUserName || "-"}</TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + </div> + <div className="mt-2 text-sm text-muted-foreground"> + 총 {filteredAndSortedConfirmed.length}건 + </div> + </CardContent> + </Card> + + {/* 업체입력정보 테이블 */} + <Card> + <CardHeader> + <CardTitle>업체입력정보</CardTitle> + <CardDescription> + 업체에서 입력한 공급 가능 품목 정보입니다. + </CardDescription> + <div className="flex items-center space-x-2"> + <Search className="h-4 w-4 text-muted-foreground" /> + <Input + placeholder="자재그룹코드 또는 자재그룹명으로 검색..." + value={vendorInputSearch} + onChange={(e) => setVendorInputSearch(e.target.value)} + className="max-w-sm" + /> + </div> + </CardHeader> + <CardContent> + <div className="rounded-md border max-h-96 overflow-auto"> + <Table> + <TableHeader className="sticky top-0 bg-background z-10"> + <TableRow> + <TableHead> + <SortButton + field="vendorTypeNameEn" + onSort={(field) => handleSort(field, vendorInputSort, setVendorInputSort)} + > + 업체유형 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="itemCode" + onSort={(field) => handleSort(field, vendorInputSort, setVendorInputSort)} + > + 자재그룹코드 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="itemName" + onSort={(field) => handleSort(field, vendorInputSort, setVendorInputSort)} + > + 자재그룹명 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="createdAt" + onSort={(field) => handleSort(field, vendorInputSort, setVendorInputSort)} + > + 등록일 + </SortButton> + </TableHead> + <TableHead> + <SortButton + field="registerUserName" + onSort={(field) => handleSort(field, vendorInputSort, setVendorInputSort)} + > + 등록자명 + </SortButton> + </TableHead> + </TableRow> + </TableHeader> + <TableBody> + {filteredAndSortedVendorInput.length === 0 ? ( + <TableRow> + <TableCell colSpan={5} className="text-center text-muted-foreground"> + {vendorInputSearch ? "검색 결과가 없습니다." : "업체입력 정보가 없습니다."} + </TableCell> + </TableRow> + ) : ( + filteredAndSortedVendorInput.map((material) => ( + <TableRow key={material.id}> + <TableCell>{material.vendorTypeNameEn || "-"}</TableCell> + <TableCell className="font-mono">{material.itemCode || "-"}</TableCell> + <TableCell>{material.itemName || "-"}</TableCell> + <TableCell>{formatDateToYMD(material.createdAt)}</TableCell> + <TableCell>{material.registerUserName || "-"}</TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + </div> + <div className="mt-2 text-sm text-muted-foreground"> + 총 {filteredAndSortedVendorInput.length}건 + </div> + </CardContent> + </Card> + </div> + ); +} diff --git a/lib/material/vendor-material/vendor-input-materials-table.tsx b/lib/material/vendor-material/vendor-input-materials-table.tsx new file mode 100644 index 00000000..8d774bee --- /dev/null +++ b/lib/material/vendor-material/vendor-input-materials-table.tsx @@ -0,0 +1,64 @@ +"use client"; + +import * as React from "react"; +import { DataTable } from "@/components/data-table/data-table"; +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; +import { vendorInputMaterialsColumns } from "./columns"; +import { useDataTable } from "@/hooks/use-data-table"; +import { VendorPossibleMaterial } from "../vendor-possible-material-service"; + +interface VendorInputMaterialsTableProps { + vendorId: number; + data: VendorPossibleMaterial[]; + pageCount: number; +} + +export function VendorInputMaterialsTable({ + vendorId, + data, + pageCount, +}: VendorInputMaterialsTableProps) { + const { table } = useDataTable({ + data, + columns: vendorInputMaterialsColumns, + pageCount, + filterFields: [ + { + id: "itemCode", + label: "자재그룹", + placeholder: "자재그룹 검색...", + }, + { + id: "itemName", + label: "자재그룹명", + placeholder: "자재그룹명 검색...", + }, + ], + }); + + return ( + <div className="space-y-4"> + <h3 className="text-lg font-semibold">업체입력정보</h3> + + <DataTable table={table}> + <DataTableToolbar + table={table} + filterFields={[ + { + id: "itemCode", + label: "자재그룹", + placeholder: "자재그룹 검색...", + }, + { + id: "itemName", + label: "자재그룹명", + placeholder: "자재그룹명 검색...", + }, + ]} + > + {/* 컬럼 선택 기능 제거 - DataTableViewOptions 없음 */} + </DataTableToolbar> + </DataTable> + </div> + ); +} diff --git a/lib/material/vendor-material/vendor-materials-client.tsx b/lib/material/vendor-material/vendor-materials-client.tsx new file mode 100644 index 00000000..412dc4b9 --- /dev/null +++ b/lib/material/vendor-material/vendor-materials-client.tsx @@ -0,0 +1,39 @@ +"use client"; + +import * as React from "react"; +import { Separator } from "@/components/ui/separator"; +import { ConfirmedMaterialsTable } from "./confirmed-materials-table"; +import { VendorInputMaterialsTable } from "./vendor-input-materials-table"; +import { VendorMaterialsResult } from "../vendor-possible-material-service"; + +interface VendorMaterialsClientProps { + vendorId: number; + confirmedMaterials: VendorMaterialsResult; + vendorInputMaterials: VendorMaterialsResult; +} + +export function VendorMaterialsClient({ + vendorId, + confirmedMaterials, + vendorInputMaterials, +}: VendorMaterialsClientProps) { + return ( + <div className="space-y-8"> + {/* 확정정보 테이블 */} + <ConfirmedMaterialsTable + vendorId={vendorId} + data={confirmedMaterials.data} + pageCount={confirmedMaterials.pageCount} + /> + + <Separator /> + + {/* 업체입력정보 테이블 */} + <VendorInputMaterialsTable + vendorId={vendorId} + data={vendorInputMaterials.data} + pageCount={vendorInputMaterials.pageCount} + /> + </div> + ); +} |
