diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-03 12:44:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-03 12:44:32 +0000 |
| commit | 688d9884ca98b50d04ac78fc1f6e28e034a519c0 (patch) | |
| tree | 95cf4572882a4d771db9443140d8cf2735d840d7 /lib/rfq-last/table/rfq-items-dialog.tsx | |
| parent | 522176a23ad9db47f85ceed13b2e54d369aa6e0a (diff) | |
(대표님) rfq-last 작업, vendorDocu 스키마 변경, 벤더 문서 관련 변경
Diffstat (limited to 'lib/rfq-last/table/rfq-items-dialog.tsx')
| -rw-r--r-- | lib/rfq-last/table/rfq-items-dialog.tsx | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/lib/rfq-last/table/rfq-items-dialog.tsx b/lib/rfq-last/table/rfq-items-dialog.tsx new file mode 100644 index 00000000..5d7e0747 --- /dev/null +++ b/lib/rfq-last/table/rfq-items-dialog.tsx @@ -0,0 +1,354 @@ +"use client" + +import * as React from "react" +import { format } from "date-fns" +import { Package, ExternalLink } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Skeleton } from "@/components/ui/skeleton" +import { Separator } from "@/components/ui/separator" +import { toast } from "sonner" +import { RfqsLastView } from "@/db/schema" +import { getRfqItemsAction } from "../service" + +// 품목 타입 +interface RfqItem { + id: number + rfqsLastId: number | null + rfqItem: string | null + prItem: string | null + prNo: string | null + materialCode: string | null + materialCategory: string | null + acc: string | null + materialDescription: string | null + size: string | null + deliveryDate: Date | null + quantity: number | null + uom: string | null + grossWeight: number | null + gwUom: string | null + specNo: string | null + specUrl: string | null + trackingNo: string | null + majorYn: boolean | null + remark: string | null + projectDef: string | null + projectSc: string | null + projectKl: string | null + projectLc: string | null + projectDl: string | null + // RFQ 관련 정보 + rfqCode: string | null + rfqType: string | null + rfqTitle: string | null + itemCode: string | null + itemName: string | null + projectCode: string | null + projectName: string | null +} + +interface ItemStatistics { + total: number + major: number + regular: number + totalQuantity: number + totalWeight: number +} + +interface RfqItemsDialogProps { + isOpen: boolean + onClose: () => void + rfqData: RfqsLastView +} + +export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps) { + const [items, setItems] = React.useState<RfqItem[]>([]) + const [statistics, setStatistics] = React.useState<ItemStatistics | null>(null) + const [isLoading, setIsLoading] = React.useState(false) + + // 품목 목록 로드 + React.useEffect(() => { + if (!isOpen || !rfqData.id) return + + const loadItems = async () => { + setIsLoading(true) + try { + const result = await getRfqItemsAction(rfqData.id) + + if (result.success) { + setItems(result.data) + setStatistics(result.statistics) + } else { + toast.error(result.error || "품목을 불러오는데 실패했습니다") + setItems([]) + setStatistics(null) + } + } catch (error) { + console.error("품목 로드 오류:", error) + toast.error("품목을 불러오는데 실패했습니다") + setItems([]) + setStatistics(null) + } finally { + setIsLoading(false) + } + } + + loadItems() + }, [isOpen, rfqData.id]) + + // 사양서 링크 열기 + const handleOpenSpec = (specUrl: string) => { + window.open(specUrl, '_blank', 'noopener,noreferrer') + } + + // 수량 포맷팅 + const formatQuantity = (quantity: number | null, uom: string | null) => { + if (!quantity) return "-" + return `${quantity.toLocaleString()}${uom ? ` ${uom}` : ""}` + } + + // 중량 포맷팅 + const formatWeight = (weight: number | null, uom: string | null) => { + if (!weight) return "-" + return `${weight.toLocaleString()} ${uom || "KG"}` + } + + return ( + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent className="max-w-7xl h-[90vh] flex flex-col"> + <DialogHeader> + <DialogTitle>견적 품목 목록</DialogTitle> + <DialogDescription> + {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "품목 정보"} + </DialogDescription> + </DialogHeader> + + {/* 통계 정보 */} + {statistics && !isLoading && ( + <> + <div className="grid grid-cols-5 gap-3 py-3"> + <div className="text-center"> + <div className="text-2xl font-bold text-primary">{statistics.total}</div> + <div className="text-xs text-muted-foreground">전체 품목</div> + </div> + <div className="text-center"> + <div className="text-2xl font-bold text-blue-600">{statistics.major}</div> + <div className="text-xs text-muted-foreground">주요 품목</div> + </div> + <div className="text-center"> + <div className="text-2xl font-bold text-gray-600">{statistics.regular}</div> + <div className="text-xs text-muted-foreground">일반 품목</div> + </div> + <div className="text-center"> + <div className="text-2xl font-bold text-green-600">{statistics.totalQuantity.toLocaleString()}</div> + <div className="text-xs text-muted-foreground">총 수량</div> + </div> + <div className="text-center"> + <div className="text-2xl font-bold text-orange-600">{statistics.totalWeight.toLocaleString()}</div> + <div className="text-xs text-muted-foreground">총 중량 (KG)</div> + </div> + </div> + <Separator /> + </> + )} + + <ScrollArea className="flex-1"> + {isLoading ? ( + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[60px]">구분</TableHead> + <TableHead className="w-[120px]">자재코드</TableHead> + <TableHead>자재명</TableHead> + <TableHead className="w-[100px]">수량</TableHead> + <TableHead className="w-[100px]">중량</TableHead> + <TableHead className="w-[100px]">납기일</TableHead> + <TableHead className="w-[100px]">PR번호</TableHead> + <TableHead className="w-[80px]">사양</TableHead> + <TableHead>비고</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {[...Array(3)].map((_, i) => ( + <TableRow key={i}> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> + </TableRow> + ))} + </TableBody> + </Table> + ) : items.length === 0 ? ( + <div className="text-center text-muted-foreground py-12"> + <Package className="h-12 w-12 mx-auto mb-4 text-muted-foreground" /> + <p>품목이 없습니다.</p> + </div> + ) : ( + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[60px]">구분</TableHead> + <TableHead className="w-[120px]">자재코드</TableHead> + <TableHead>자재명</TableHead> + <TableHead className="w-[100px]">수량</TableHead> + <TableHead className="w-[100px]">중량</TableHead> + <TableHead className="w-[100px]">납기일</TableHead> + <TableHead className="w-[100px]">PR번호</TableHead> + <TableHead className="w-[100px]">사양</TableHead> + <TableHead className="w-[100px]">프로젝트</TableHead> + <TableHead>비고</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {items.map((item, index) => ( + <TableRow key={item.id} className={item.majorYn ? "bg-blue-50 border-l-4 border-l-blue-500" : ""}> + <TableCell> + <div className="flex flex-col items-center gap-1"> + <span className="text-xs font-mono">#{index + 1}</span> + {item.majorYn && ( + <Badge variant="default" className="text-xs px-1 py-0"> + 주요 + </Badge> + )} + </div> + </TableCell> + <TableCell> + <div className="flex flex-col"> + <span className="font-mono text-sm font-medium">{item.materialCode || "-"}</span> + {item.acc && ( + <span className="text-xs text-muted-foreground font-mono"> + ACC: {item.acc} + </span> + )} + </div> + </TableCell> + <TableCell> + <div className="flex flex-col"> + <span className="text-sm font-medium" title={item.materialDescription || ""}> + {item.materialDescription || "-"} + </span> + {item.materialCategory && ( + <span className="text-xs text-muted-foreground"> + {item.materialCategory} + </span> + )} + {item.size && ( + <span className="text-xs text-muted-foreground"> + 크기: {item.size} + </span> + )} + </div> + </TableCell> + <TableCell> + <span className="text-sm font-medium"> + {formatQuantity(item.quantity, item.uom)} + </span> + </TableCell> + <TableCell> + <span className="text-sm"> + {formatWeight(item.grossWeight, item.gwUom)} + </span> + </TableCell> + <TableCell> + <span className="text-sm"> + {item.deliveryDate ? format(new Date(item.deliveryDate), "yyyy-MM-dd") : "-"} + </span> + </TableCell> + <TableCell> + <div className="flex flex-col"> + <span className="text-xs font-mono">{item.prNo || "-"}</span> + {item.prItem && item.prItem !== item.prNo && ( + <span className="text-xs text-muted-foreground font-mono"> + {item.prItem} + </span> + )} + </div> + </TableCell> + <TableCell> + <div className="flex items-center gap-1"> + {item.specNo && ( + <span className="text-xs font-mono">{item.specNo}</span> + )} + {item.specUrl && ( + <Button + variant="ghost" + size="sm" + className="h-5 w-5 p-0" + onClick={() => handleOpenSpec(item.specUrl!)} + title="사양서 열기" + > + <ExternalLink className="h-3 w-3" /> + </Button> + )} + {item.trackingNo && ( + <div className="text-xs text-muted-foreground"> + TRK: {item.trackingNo} + </div> + )} + </div> + </TableCell> + <TableCell> + <div className="text-xs"> + {[ + item.projectDef && `DEF: ${item.projectDef}`, + item.projectSc && `SC: ${item.projectSc}`, + item.projectKl && `KL: ${item.projectKl}`, + item.projectLc && `LC: ${item.projectLc}`, + item.projectDl && `DL: ${item.projectDl}` + ].filter(Boolean).join(" | ") || "-"} + </div> + </TableCell> + <TableCell> + <span className="text-xs" title={item.remark || ""}> + {item.remark ? (item.remark.length > 30 ? `${item.remark.slice(0, 30)}...` : item.remark) : "-"} + </span> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + )} + </ScrollArea> + + {/* 하단 통계 정보 */} + {statistics && !isLoading && ( + <div className="border-t pt-3 text-xs text-muted-foreground"> + <div className="flex justify-between items-center"> + <span> + 총 {statistics.total}개 품목 + (주요: {statistics.major}개, 일반: {statistics.regular}개) + </span> + <span> + 전체 수량: {statistics.totalQuantity.toLocaleString()} | + 전체 중량: {statistics.totalWeight.toLocaleString()} KG + </span> + </div> + </div> + )} + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
