diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-26 12:09:39 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-26 12:09:39 +0000 |
| commit | 1110427907bbe9c11a378da4c1a233b83b5ca3b1 (patch) | |
| tree | 8bd7ed2ce7ec47a7f05693f5d3afcc22b1bb7e19 /lib/po | |
| parent | 5f479f7252a7aa3328bfe186893de8b011e21b15 (diff) | |
(김준회) 구매정의서 구현 - PO (shi & vendor)
Diffstat (limited to 'lib/po')
| -rw-r--r-- | lib/po/vendor-table/mock-data.ts | 1940 | ||||
| -rw-r--r-- | lib/po/vendor-table/service.ts | 417 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-columns.tsx | 462 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-table.tsx | 267 | ||||
| -rw-r--r-- | lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx | 66 | ||||
| -rw-r--r-- | lib/po/vendor-table/types.ts | 119 | ||||
| -rw-r--r-- | lib/po/vendor-table/validations.ts | 58 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-columns.tsx | 511 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-items-dialog.tsx | 199 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-table.tsx | 247 | ||||
| -rw-r--r-- | lib/po/vendor-table/vendor-po-toolbar-actions.tsx | 214 |
11 files changed, 4500 insertions, 0 deletions
diff --git a/lib/po/vendor-table/mock-data.ts b/lib/po/vendor-table/mock-data.ts new file mode 100644 index 00000000..a932ccbb --- /dev/null +++ b/lib/po/vendor-table/mock-data.ts @@ -0,0 +1,1940 @@ +import { VendorPO, VendorPOItem } from "./types" + +// 상세품목 목업 데이터 +export const mockVendorPOItems: VendorPOItem[] = [ + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-002", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-002", + itemDescription: "Steel Angle 50x50x5", + materialSpec: "SS400, 50x50x5mm x 6000mm", + specification: "50x50x5mm x 6000mm", + quantity: 200, + quantityUnit: "M", + weight: 3.77, + weightUnit: "KG/M", + totalWeight: 754.0, + unitPrice: 8000, + priceUnit: "KRW", + priceUnitValue: "원/M", + contractAmount: 1600000, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, + { + contractNo: "PO-2024-001", + itemNo: "ITM-001", + prNo: "PR-2024-0001", + materialGroup: "Steel Materials", + priceStandard: "FOB", + materialNo: "MAT-ST-001", + itemDescription: "Steel Plate 10mm", + materialSpec: "SS400, 10mm x 2000mm x 6000mm", + fittingNo: "FIT-001", + cert: "Mill Certificate", + material: "SS400", + specification: "10mm x 2000mm x 6000mm", + quantity: 100, + quantityUnit: "EA", + weight: 942.0, + weightUnit: "KG", + totalWeight: 94200.0, + unitPrice: 50000, + priceUnit: "KRW", + priceUnitValue: "원/EA", + contractAmount: 5000000, + adjustmentAmount: 0, + deliveryDate: "2024-03-15", + vatType: "과세", + steelSpec: "KS D 3503", + prManager: "김철수" + }, +] + +// 벤더 PO 목업 데이터 +export const mockVendorPOs: VendorPO[] = [ + { + id: 1, + contractNo: "PO-2024-001", + revision: "Rev.01", + itemNo: "ITM-001", + contractStatus: "승인대기", + contractType: "구매계약", + details: "상세보기", + projectName: "해상풍력 프로젝트 A", + contractName: "철강재 공급계약", + contractPeriod: "2024-01-01 ~ 2024-12-31", + contractQuantity: "300 EA", + currency: "KRW", + paymentTerms: "월말결제", + tax: "10%", + exchangeRate: "1,330.50", + deliveryTerms: "FOB", + purchaseManager: "이구매", + poReceiveDate: "2024-01-15", + contractDate: "2024-01-20", + lcNo: "LC-2024-001", + priceIndexTarget: true, + linkedContractNo: "PO-2024-000", + lastModifiedDate: "2024-01-25", + lastModifiedBy: "김수정", + items: mockVendorPOItems.filter(item => item.contractNo === "PO-2024-001") + }, + { + id: 2, + contractNo: "PO-2024-002", + revision: "Rev.00", + itemNo: "ITM-003", + contractStatus: "계약완료", + contractType: "서비스계약", + details: "상세보기", + projectName: "해상풍력 프로젝트 B", + contractName: "설치 서비스 계약", + contractPeriod: "2024-02-01 ~ 2024-06-30", + contractQuantity: "1 LOT", + currency: "USD", + paymentTerms: "선급금 30%, 잔금 70%", + tax: "면세", + exchangeRate: "1,330.50", + deliveryTerms: "CIF", + purchaseManager: "박구매", + poReceiveDate: "2024-02-01", + contractDate: "2024-02-05", + priceIndexTarget: false, + lastModifiedDate: "2024-02-10", + lastModifiedBy: "이수정", + items: [] + }, + { + id: 3, + contractNo: "PO-2024-003", + revision: "Rev.02", + itemNo: "ITM-004", + contractStatus: "수정요청", + contractType: "구매계약", + details: "상세보기", + projectName: "조선 프로젝트 C", + contractName: "선박용 부품 공급계약", + contractPeriod: "2024-03-01 ~ 2024-09-30", + contractQuantity: "150 SET", + currency: "EUR", + paymentTerms: "인도 후 30일", + tax: "10%", + exchangeRate: "1,450.20", + deliveryTerms: "EXW", + purchaseManager: "최구매", + poReceiveDate: "2024-03-01", + contractDate: "2024-03-10", + lcNo: "LC-2024-003", + priceIndexTarget: true, + linkedContractNo: "PO-2024-002", + lastModifiedDate: "2024-03-15", + lastModifiedBy: "정수정", + items: [] + }, + { + id: 4, + contractNo: "PO-2024-004", + revision: "Rev.00", + itemNo: "ITM-005", + contractStatus: "거절됨", + contractType: "임가공계약", + details: "상세보기", + projectName: "플랜트 프로젝트 D", + contractName: "가공 서비스 계약", + contractPeriod: "2024-04-01 ~ 2024-08-31", + contractQuantity: "500 HR", + currency: "KRW", + paymentTerms: "월 단위 정산", + tax: "10%", + exchangeRate: "1,330.50", + deliveryTerms: "DDP", + purchaseManager: "김구매", + poReceiveDate: "2024-04-01", + contractDate: "", + priceIndexTarget: false, + lastModifiedDate: "2024-04-05", + lastModifiedBy: "박수정", + items: [] + }, + { + id: 5, + contractNo: "PO-2024-005", + revision: "Rev.01", + itemNo: "ITM-006", + contractStatus: "진행중", + contractType: "구매계약", + details: "상세보기", + projectName: "해상풍력 프로젝트 E", + contractName: "전기설비 공급계약", + contractPeriod: "2024-05-01 ~ 2024-11-30", + contractQuantity: "25 SET", + currency: "USD", + paymentTerms: "LC 90일", + tax: "면세", + exchangeRate: "1,330.50", + deliveryTerms: "FOB", + purchaseManager: "이구매", + poReceiveDate: "2024-05-01", + contractDate: "2024-05-10", + lcNo: "LC-2024-005", + priceIndexTarget: true, + lastModifiedDate: "2024-05-15", + lastModifiedBy: "최수정", + items: [] + } +] + +// 페이지네이션을 위한 헬퍼 함수 +export function getVendorPOsPage( + page: number = 1, + perPage: number = 10, + search?: string, + filters?: any[] +): { data: VendorPO[]; pageCount: number; total: number } { + let filteredData = [...mockVendorPOs] + + // 검색 필터링 + if (search && search.trim()) { + const searchTerm = search.toLowerCase() + filteredData = filteredData.filter(po => + po.contractNo.toLowerCase().includes(searchTerm) || + po.contractName.toLowerCase().includes(searchTerm) || + po.projectName.toLowerCase().includes(searchTerm) || + po.contractStatus.toLowerCase().includes(searchTerm) + ) + } + + // 추가 필터 적용 (필요시 구현) + // if (filters && filters.length > 0) { + // // 필터 로직 구현 + // } + + const total = filteredData.length + const pageCount = Math.ceil(total / perPage) + const offset = (page - 1) * perPage + const data = filteredData.slice(offset, offset + perPage) + + return { data, pageCount, total } +} diff --git a/lib/po/vendor-table/service.ts b/lib/po/vendor-table/service.ts new file mode 100644 index 00000000..88f6ddd5 --- /dev/null +++ b/lib/po/vendor-table/service.ts @@ -0,0 +1,417 @@ +"use server"; + +import { GetVendorPOSchema } from "./validations"; +import { getVendorPOsPage } from "./mock-data"; +import { VendorPO, VendorPOItem } from "./types"; +import db from "@/db/db"; +import { contracts, contractItems } from "@/db/schema/contract"; +import { projects } from "@/db/schema/projects"; +import { vendors } from "@/db/schema/vendors"; +import { items } from "@/db/schema/items"; +import { eq, and, or, ilike, count, desc, asc } from "drizzle-orm"; + +/** + * 벤더 PO 목록 조회 + * contracts 테이블에서 실제 데이터를 조회합니다. + */ +export async function getVendorPOs(input: GetVendorPOSchema) { + try { + // 실제 데이터베이스 조회 + const offset = (input.page - 1) * input.perPage; + + // 검색 조건 구성 + let whereConditions = []; + if (input.search) { + const searchTerm = `%${input.search}%`; + whereConditions.push( + or( + ilike(contracts.contractNo, searchTerm), + ilike(contracts.contractName, searchTerm), + ilike(projects.name, searchTerm), + ilike(vendors.vendorName, searchTerm) + ) + ); + } + + // 벤더 필터링 (partners 페이지에서 사용) + if (input.vendorId && input.vendorId > 0) { + whereConditions.push(eq(contracts.vendorId, input.vendorId)); + } + + // 필터 조건 추가 + if (input.filters && input.filters.length > 0) { + for (const filter of input.filters) { + if (filter.id && filter.value) { + switch (filter.id) { + case "contractStatus": + whereConditions.push(ilike(contracts.status, `%${filter.value}%`)); + break; + case "contractType": + whereConditions.push(ilike(contracts.purchaseDocType, `%${filter.value}%`)); + break; + case "currency": + whereConditions.push(eq(contracts.currency, filter.value)); + break; + // 추가 필터 조건들... + } + } + } + } + + const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; + + // 정렬 조건 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => { + switch (item.id) { + case "contractNo": + return item.desc ? desc(contracts.contractNo) : asc(contracts.contractNo); + case "contractName": + return item.desc ? desc(contracts.contractName) : asc(contracts.contractName); + case "lastModifiedDate": + return item.desc ? desc(contracts.updatedAt) : asc(contracts.updatedAt); + default: + return desc(contracts.updatedAt); + } + }) + : [desc(contracts.updatedAt)]; + + // 데이터 조회 (조인 포함) + const rawData = await db + .select({ + // contracts 테이블 필드들 + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + startDate: contracts.startDate, + endDate: contracts.endDate, + currency: contracts.currency, + totalAmount: contracts.totalAmount, + totalAmountKrw: contracts.totalAmountKrw, + paymentTerms: contracts.paymentTerms, + deliveryTerms: contracts.deliveryTerms, + deliveryLocation: contracts.deliveryLocation, + shippmentPlace: contracts.shippmentPlace, + exchangeRate: contracts.exchangeRate, + + // SAP ECC 추가 필드들 + poVersion: contracts.poVersion, + purchaseDocType: contracts.purchaseDocType, + purchaseOrg: contracts.purchaseOrg, + purchaseGroup: contracts.purchaseGroup, + poConfirmStatus: contracts.poConfirmStatus, + contractGuaranteeCode: contracts.contractGuaranteeCode, + defectGuaranteeCode: contracts.defectGuaranteeCode, + guaranteePeriodCode: contracts.guaranteePeriodCode, + advancePaymentYn: contracts.advancePaymentYn, + budgetAmount: contracts.budgetAmount, + budgetCurrency: contracts.budgetCurrency, + electronicContractYn: contracts.electronicContractYn, + electronicApprovalDate: contracts.electronicApprovalDate, + electronicApprovalTime: contracts.electronicApprovalTime, + ownerApprovalYn: contracts.ownerApprovalYn, + plannedInOutFlag: contracts.plannedInOutFlag, + settlementStandard: contracts.settlementStandard, + weightSettlementFlag: contracts.weightSettlementFlag, + priceIndexYn: contracts.priceIndexYn, + writtenContractNo: contracts.writtenContractNo, + contractVersion: contracts.contractVersion, + + createdAt: contracts.createdAt, + updatedAt: contracts.updatedAt, + + // 조인된 테이블 필드들 + projectName: projects.name, + vendorName: vendors.vendorName, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(finalWhere) + .orderBy(...orderBy) + .offset(offset) + .limit(input.perPage); + + // 총 개수 조회 + const [{ totalCount }] = await db + .select({ totalCount: count() }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(finalWhere); + + const pageCount = Math.ceil(totalCount / input.perPage); + + // VendorPO 타입으로 변환 + const data: VendorPO[] = rawData.map(row => ({ + id: row.id, + contractNo: row.contractNo || '', + revision: 'Rev.01', // mock 데이터용 기본값 + itemNo: 'ITM-AUTO', // mock 데이터용 기본값 + contractStatus: row.status || '', + contractType: row.purchaseDocType || '', + details: '상세보기', // mock 데이터용 기본값 + projectName: row.projectName || '', + contractName: row.contractName || '', + contractPeriod: row.startDate && row.endDate + ? `${row.startDate} ~ ${row.endDate}` + : '', + contractQuantity: '1 LOT', // 기본값 (실제로는 contract_items에서 계산 필요) + currency: row.currency || 'KRW', + paymentTerms: row.paymentTerms || '', + tax: '10%', // 기본값 (실제로는 contract_items에서 계산 필요) + exchangeRate: row.exchangeRate?.toString() || '', + deliveryTerms: row.deliveryTerms || '', + purchaseManager: '', // 사용자 테이블 조인 필요 + poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', + contractDate: row.startDate || '', + lcNo: undefined, + priceIndexTarget: row.priceIndexYn === 'Y', + linkedContractNo: undefined, + lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', + lastModifiedBy: '', // 사용자 테이블 조인 필요 + + // SAP ECC 추가 필드들 + poVersion: row.poVersion || undefined, + purchaseDocType: row.purchaseDocType || undefined, + purchaseOrg: row.purchaseOrg || undefined, + purchaseGroup: row.purchaseGroup || undefined, + poConfirmStatus: row.poConfirmStatus || undefined, + contractGuaranteeCode: row.contractGuaranteeCode || undefined, + defectGuaranteeCode: row.defectGuaranteeCode || undefined, + guaranteePeriodCode: row.guaranteePeriodCode || undefined, + advancePaymentYn: row.advancePaymentYn || undefined, + budgetAmount: row.budgetAmount ? Number(row.budgetAmount) : undefined, + budgetCurrency: row.budgetCurrency || undefined, + totalAmount: row.totalAmount ? Number(row.totalAmount) : undefined, + totalAmountKrw: row.totalAmountKrw ? Number(row.totalAmountKrw) : undefined, + electronicContractYn: row.electronicContractYn || undefined, + electronicApprovalDate: row.electronicApprovalDate || undefined, + electronicApprovalTime: row.electronicApprovalTime || undefined, + ownerApprovalYn: row.ownerApprovalYn || undefined, + plannedInOutFlag: row.plannedInOutFlag || undefined, + settlementStandard: row.settlementStandard || undefined, + weightSettlementFlag: row.weightSettlementFlag || undefined, + priceIndexYn: row.priceIndexYn || undefined, + writtenContractNo: row.writtenContractNo || undefined, + contractVersion: row.contractVersion || undefined, + })); + + return { + data, + pageCount + }; + + // 목업 데이터 사용 (개발/테스트용) + // const result = getVendorPOsPage( + // input.page, + // input.perPage, + // input.search, + // input.filters + // ); + + // 실제 데이터베이스 연동시에는 아래와 같은 구조로 구현 + // const offset = (input.page - 1) * input.perPage; + // + // // 검색 조건 구성 + // let whereConditions = []; + // if (input.search) { + // const searchTerm = `%${input.search}%`; + // whereConditions.push( + // or( + // ilike(vendorPOTable.contractNo, searchTerm), + // ilike(vendorPOTable.contractName, searchTerm), + // ilike(vendorPOTable.projectName, searchTerm) + // ) + // ); + // } + // + // // 필터 조건 추가 + // if (input.contractStatus) { + // whereConditions.push(eq(vendorPOTable.contractStatus, input.contractStatus)); + // } + // + // const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; + // + // // 정렬 조건 + // const orderBy = input.sort.length > 0 + // ? input.sort.map((item) => + // item.desc + // ? desc(vendorPOTable[item.id]) + // : asc(vendorPOTable[item.id]) + // ) + // : [desc(vendorPOTable.lastModifiedDate)]; + // + // // 데이터 조회 + // const data = await db + // .select() + // .from(vendorPOTable) + // .where(finalWhere) + // .orderBy(...orderBy) + // .offset(offset) + // .limit(input.perPage); + // + // // 총 개수 조회 + // const [{ count }] = await db + // .select({ count: count() }) + // .from(vendorPOTable) + // .where(finalWhere); + // + // const pageCount = Math.ceil(count / input.perPage); + + return { + data: result.data, + pageCount: result.pageCount + }; + } catch (err) { + console.error("Error in getVendorPOs:", err); + return { data: [], pageCount: 0 }; + } +} + +/** + * 벤더 PO 상세 정보 조회 + */ +export async function getVendorPOById(id: number): Promise<VendorPO | null> { + try { + // 목업 데이터에서 조회 + const result = getVendorPOsPage(1, 100); // 모든 데이터 가져오기 + const po = result.data.find(item => item.id === id); + + return po || null; + } catch (err) { + console.error("Error in getVendorPOById:", err); + return null; + } +} + +/** + * 벤더 PO 액션 처리 + * PCR생성, 승인, 거절 등의 액션을 처리 + */ +export async function handleVendorPOAction( + poId: number, + action: string, + data?: any +): Promise<{ success: boolean; message: string }> { + try { + // 목업에서는 성공 응답만 반환 + // 실제 구현시에는 각 액션별로 비즈니스 로직 구현 + + switch (action) { + case "pcr-create": + return { success: true, message: "PCR이 성공적으로 생성되었습니다." }; + case "approve": + return { success: true, message: "계약이 승인되었습니다." }; + case "cancel-approve": + return { success: true, message: "승인이 취소되었습니다." }; + case "reject-contract": + return { success: true, message: "계약이 거절되었습니다." }; + case "print-contract": + return { success: true, message: "계약서 출력이 요청되었습니다." }; + default: + return { success: false, message: "알 수 없는 액션입니다." }; + } + } catch (err) { + console.error("Error in handleVendorPOAction:", err); + return { success: false, message: "액션 처리 중 오류가 발생했습니다." }; + } +} + +/** + * 특정 계약의 상세품목 조회 + * contract_items 테이블에서 실제 데이터를 조회합니다. + */ +export async function getVendorPOItems(contractId: number): Promise<VendorPOItem[]> { + try { + const rawItems = await db + .select({ + // contract_items 테이블 필드들 + id: contractItems.id, + contractId: contractItems.contractId, + itemId: contractItems.itemId, + description: contractItems.description, + quantity: contractItems.quantity, + unitPrice: contractItems.unitPrice, + taxRate: contractItems.taxRate, + taxAmount: contractItems.taxAmount, + totalLineAmount: contractItems.totalLineAmount, + remark: contractItems.remark, + + // contracts 테이블 필드들 + contractNo: contracts.contractNo, + + // items 테이블 필드들 + itemCode: items.itemCode, + itemName: items.itemName, + packageCode: items.packageCode, + unitOfMeasure: items.unitOfMeasure, + gradeMaterial: items.gradeMaterial, + steelType: items.steelType, + smCode: items.smCode, + }) + .from(contractItems) + .leftJoin(contracts, eq(contractItems.contractId, contracts.id)) + .leftJoin(items, eq(contractItems.itemId, items.id)) + .where(eq(contractItems.contractId, contractId)) + .orderBy(contractItems.id); + + // VendorPOItem 타입으로 변환 + const vendorPOItems: VendorPOItem[] = rawItems.map(row => ({ + contractNo: row.contractNo || '', + itemNo: row.itemCode || 'AUTO-ITEM', // mock 데이터용 + prNo: `PR-${new Date().getFullYear()}-${String(row.id).padStart(4, '0')}`, // mock 데이터용 + materialGroup: row.packageCode || 'Unknown Group', + priceStandard: 'FOB', // mock 데이터용 기본값 + materialNo: row.itemCode || '', + itemDescription: row.itemName || '', + materialSpec: row.description || '', + fittingNo: undefined, // contract_items에 없는 필드 + cert: undefined, // contract_items에 없는 필드 + material: row.gradeMaterial || undefined, + specification: row.description || '', + quantity: row.quantity || 1, + quantityUnit: row.unitOfMeasure || 'EA', + weight: undefined, // contract_items에 없는 필드 + weightUnit: undefined, // contract_items에 없는 필드 + totalWeight: undefined, // contract_items에 없는 필드 + unitPrice: row.unitPrice ? Number(row.unitPrice) : 0, + priceUnit: 'KRW', // 기본값 + priceUnitValue: '원/EA', // 기본값 + contractAmount: row.totalLineAmount ? Number(row.totalLineAmount) : 0, + adjustmentAmount: undefined, // contract_items에 없는 필드 + deliveryDate: new Date().toISOString().split('T')[0], // 기본값 (오늘 날짜) + vatType: row.taxRate && Number(row.taxRate) > 0 ? '과세' : '면세', + steelSpec: row.steelType || undefined, + prManager: 'AUTO-MANAGER', // mock 데이터용 기본값 + })); + + return vendorPOItems; + } catch (err) { + console.error("Error in getVendorPOItems:", err); + throw err; + } +} + +/** + * 계약번호로 상세품목 조회 (외부에서 contractNo로 호출할 때 사용) + */ +export async function getVendorPOItemsByContractNo(contractNo: string): Promise<VendorPOItem[]> { + try { + // 먼저 계약 ID 조회 + const contract = await db.query.contracts.findFirst({ + where: eq(contracts.contractNo, contractNo), + }); + + if (!contract) { + console.warn(`Contract not found: ${contractNo}`); + return []; + } + + return await getVendorPOItems(contract.id); + } catch (err) { + console.error("Error in getVendorPOItemsByContractNo:", err); + throw err; + } +} diff --git a/lib/po/vendor-table/shi-vendor-po-columns.tsx b/lib/po/vendor-table/shi-vendor-po-columns.tsx new file mode 100644 index 00000000..041e0c05 --- /dev/null +++ b/lib/po/vendor-table/shi-vendor-po-columns.tsx @@ -0,0 +1,462 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { + SendIcon, + FileTextIcon, + MoreHorizontalIcon, +} from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + +import { VendorPO } from "./types" + +// 서명 요청 전용 액션 타입 +type ShiVendorPORowAction = { + row: { original: VendorPO } + type: "view-items" | "signature-request" +} + +interface GetShiVendorColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<ShiVendorPORowAction | null>> + selectedRows: number[] + onRowSelect: (id: number, selected: boolean) => void +} + +export function getShiVendorColumns({ + setRowAction, + selectedRows, + onRowSelect, +}: GetShiVendorColumnsProps): ColumnDef<VendorPO>[] { + return [ + // 선택 체크박스 (1개만 선택 가능) + { + id: "select", + header: () => <div className="text-center">선택</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Checkbox + checked={selectedRows.includes(row.original.id)} + onCheckedChange={(checked) => { + onRowSelect(row.original.id, !!checked) + }} + aria-label="행 선택" + /> + </div> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + + // No. (ID) + { + accessorKey: "id", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="No." /> + ), + cell: ({ row }) => { + const id = row.getValue("id") as number + return <div className="text-sm font-mono">{id}</div> + }, + size: 60, + }, + + // PO/계약번호 + { + accessorKey: "contractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약번호" /> + ), + cell: ({ row }) => { + const contractNo = row.getValue("contractNo") as string + return ( + <div className="font-medium"> + {contractNo} + </div> + ) + }, + size: 120, + }, + + // Rev. / 품번 (PO 버전) + { + accessorKey: "poVersion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Rev. / 품번" /> + ), + cell: ({ row }) => { + const version = row.getValue("poVersion") as number + return <div className="text-sm font-medium">{version || '-'}</div> + }, + size: 80, + }, + + // 계약상태 + { + accessorKey: "contractStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("contractStatus") as string + return ( + <Badge variant="outline"> + {status || '-'} + </Badge> + ) + }, + size: 100, + }, + + // 계약종류 + { + accessorKey: "contractType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약종류" /> + ), + cell: ({ row }) => { + const type = row.getValue("contractType") as string + return <Badge variant="outline">{type || '-'}</Badge> + }, + size: 100, + }, + + // 상세품목 (버튼) + { + id: "itemsAction", + header: () => <div className="text-center">상세품목</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="h-3.5 w-3.5 mr-1" /> + 보기 + </Button> + </div> + ), + enableSorting: false, + size: 80, + }, + + // 프로젝트 + { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => { + const projectName = row.getValue("projectName") as string + return ( + <div className="max-w-[150px] truncate" title={projectName}> + {projectName || '-'} + </div> + ) + }, + size: 150, + }, + + // 계약명/자재내역 + { + accessorKey: "contractName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약명/자재내역" /> + ), + cell: ({ row }) => { + const contractName = row.getValue("contractName") as string + return ( + <div className="max-w-[200px] truncate" title={contractName}> + {contractName || '-'} + </div> + ) + }, + size: 200, + }, + + // PO/계약기간 + { + accessorKey: "contractPeriod", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약기간" /> + ), + cell: ({ row }) => { + const period = row.getValue("contractPeriod") as string + return ( + <div className="text-sm whitespace-nowrap"> + {period || '-'} + </div> + ) + }, + size: 150, + }, + + // PO/계약금액 + { + accessorKey: "totalAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약금액" /> + ), + cell: ({ row }) => { + const amount = row.getValue("totalAmount") as string | number + return <div className="text-sm text-right font-mono">{amount || '-'}</div> + }, + size: 120, + }, + + // 계약통화 + { + accessorKey: "currency", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약통화" /> + ), + cell: ({ row }) => { + const currency = row.getValue("currency") as string + return <div className="text-sm font-mono">{currency || '-'}</div> + }, + size: 80, + }, + + // 지불조건 + { + accessorKey: "paymentTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="지불조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("paymentTerms") as string + return ( + <div className="max-w-[120px] truncate text-sm" title={terms}> + {terms || '-'} + </div> + ) + }, + size: 120, + }, + + // Tax + { + accessorKey: "tax", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Tax" /> + ), + cell: ({ row }) => { + const tax = row.getValue("tax") as string + return <div className="text-sm">{tax || '-'}</div> + }, + size: 80, + }, + + // 환율 + { + accessorKey: "exchangeRate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="환율" /> + ), + cell: ({ row }) => { + const rate = row.getValue("exchangeRate") as string + return <div className="text-sm font-mono">{rate || '-'}</div> + }, + size: 100, + }, + + // 인도조건 + { + accessorKey: "deliveryTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="인도조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("deliveryTerms") as string + return <div className="text-sm">{terms || '-'}</div> + }, + size: 100, + }, + + // 구매/계약담당 + { + accessorKey: "purchaseManager", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="구매/계약담당" /> + ), + cell: ({ row }) => { + const manager = row.getValue("purchaseManager") as string + return <div className="text-sm">{manager || '-'}</div> + }, + size: 120, + }, + + // PO/계약수신일 + { + accessorKey: "poReceiveDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약수신일" /> + ), + cell: ({ row }) => { + const date = row.getValue("poReceiveDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 계약체결일 + { + accessorKey: "contractDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약체결일" /> + ), + cell: ({ row }) => { + const date = row.getValue("contractDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // L/C No. + { + accessorKey: "lcNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="L/C No." /> + ), + cell: ({ row }) => { + const lcNo = row.getValue("lcNo") as string + return <div className="text-sm">{lcNo || '-'}</div> + }, + size: 120, + }, + + // 납품대금 연동제 대상 + { + accessorKey: "priceIndexTarget", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="납품대금 연동제 대상" /> + ), + cell: ({ row }) => { + const target = row.getValue("priceIndexTarget") as string | boolean + return <div className="text-sm">{target?.toString() || '-'}</div> + }, + size: 140, + }, + + // 연계 PO/계약번호 + { + accessorKey: "linkedContractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="연계 PO/계약번호" /> + ), + cell: ({ row }) => { + const linkedNo = row.getValue("linkedContractNo") as string + return <div className="text-sm">{linkedNo || '-'}</div> + }, + size: 140, + }, + + // 최종수정일 + { + accessorKey: "lastModifiedDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("lastModifiedDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 최종수정자 + { + accessorKey: "lastModifiedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정자" /> + ), + cell: ({ row }) => { + const user = row.getValue("lastModifiedBy") as string + return <div className="text-sm">{user || '-'}</div> + }, + size: 120, + }, + + // 액션 컬럼 (서명 요청 중심) + { + id: "actions", + enableHiding: false, + header: () => <div className="text-center">Actions</div>, + cell: ({ row }) => { + return ( + <div className="flex items-center justify-center gap-2"> + {/* 서명 요청 버튼 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2 text-blue-600 border-blue-200 hover:bg-blue-50" + onClick={() => setRowAction({ row, type: "signature-request" })} + > + <SendIcon className="h-3.5 w-3.5 mr-1" aria-hidden="true" /> + 서명 요청 + </Button> + </TooltipTrigger> + <TooltipContent> + 벤더에게 전자서명 요청 + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 드롭다운 메뉴 (추가 액션) + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">Open menu</span> + <MoreHorizontalIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>액션</DropdownMenuLabel> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="mr-2 h-4 w-4" /> + 상세품목 보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "signature-request" })} + className="text-blue-600" + > + <SendIcon className="mr-2 h-4 w-4" /> + 서명 요청 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> */} + </div> + ) + }, + size: 160, + minSize: 160, + }, + ] +}
\ No newline at end of file diff --git a/lib/po/vendor-table/shi-vendor-po-table.tsx b/lib/po/vendor-table/shi-vendor-po-table.tsx new file mode 100644 index 00000000..851f831e --- /dev/null +++ b/lib/po/vendor-table/shi-vendor-po-table.tsx @@ -0,0 +1,267 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { toast } from "sonner" + +import { getVendorPOs, handleVendorPOAction } from "./service" +import { getShiVendorColumns } from "./shi-vendor-po-columns" +import { VendorPO, VendorPOActionType } from "./types" +import { VendorPOItemsDialog } from "./vendor-po-items-dialog" +import { ShiVendorPOToolbarActions } from "./shi-vendor-po-toolbar-actions" +import { SignatureRequestModal } from "@/lib/po/table/sign-request-dialog" + +interface ShiVendorPoTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getVendorPOs>>, + ] + > +} + +// Interface for signing party (서명 요청에 사용) +interface SigningParty { + signerEmail: string; + signerName: string; + signerPosition: string; + signerType: "REQUESTER" | "VENDOR"; + vendorContactId?: number; +} + +export function ShiVendorPoTable({ promises }: ShiVendorPoTableProps) { + const [data, setData] = React.useState<{ + data: VendorPO[]; + pageCount: number; + }>({ data: [], pageCount: 0 }); + + const [selectedRows, setSelectedRows] = React.useState<number[]>([]) + + // 데이터 로딩 + React.useEffect(() => { + promises.then(([result]) => { + console.log("Vendor PO data:", result.data) + setData(result); + }); + }, [promises]); + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<VendorPO> | null>(null) + + // 다이얼로그 상태 + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [signatureModalOpen, setSignatureModalOpen] = React.useState(false) + const [selectedPO, setSelectedPO] = React.useState<VendorPO | null>(null) + + // 행 선택 처리 (1개만 선택 가능) + const handleRowSelect = (id: number, selected: boolean) => { + if (selected) { + setSelectedRows([id]) // 1개만 선택 + } else { + setSelectedRows([]) + } + } + + // 행 액션 처리 + React.useEffect(() => { + if (!rowAction) return + + const po = rowAction.row.original + setSelectedPO(po) + + switch (rowAction.type as VendorPOActionType) { + case "view-items": + setItemsDialogOpen(true) + break + case "signature-request": + setSignatureModalOpen(true) + break + case "item-status": + setItemsDialogOpen(true) + break + default: + toast.info("해당 기능은 개발 중입니다.") + } + + setRowAction(null) + }, [rowAction]) + + // 액션 처리 함수 + const handleAction = async (poId: number, action: string) => { + try { + const result = await handleVendorPOAction(poId, action) + if (result.success) { + toast.success(result.message) + // 필요시 데이터 새로고침 + } else { + toast.error(result.message) + } + } catch (error) { + console.error("Action error:", error) + toast.error("액션 처리 중 오류가 발생했습니다.") + } + } + + // 서명 요청 처리 함수 + const handleSignatureRequest = async ( + values: { signers: SigningParty[] }, + contractId: number + ): Promise<void> => { + try { + // TODO: 실제 서명 요청 API 호출 + console.log("Signature request for contract:", contractId, values); + toast.success("서명 요청이 성공적으로 전송되었습니다."); + } catch (error) { + console.error("Error sending signature requests:", error); + toast.error("서명 요청 전송 중 오류가 발생했습니다."); + } + } + + const columns = React.useMemo( + () => getShiVendorColumns({ + setRowAction, + selectedRows, + onRowSelect: handleRowSelect + }), + [selectedRows] + ) + + const filterFields: DataTableFilterField<VendorPO>[] = [ + { + id: "contractStatus", + label: "계약상태", + options: [ + { label: "승인대기", value: "승인대기" }, + { label: "계약완료", value: "계약완료" }, + { label: "진행중", value: "진행중" }, + { label: "수정요청", value: "수정요청" }, + { label: "거절됨", value: "거절됨" }, + ] + }, + { + id: "contractType", + label: "계약종류", + options: [ + { label: "구매계약", value: "구매계약" }, + { label: "서비스계약", value: "서비스계약" }, + { label: "임가공계약", value: "임가공계약" }, + ] + }, + { + id: "currency", + label: "계약통화", + options: [ + { label: "KRW", value: "KRW" }, + { label: "USD", value: "USD" }, + { label: "EUR", value: "EUR" }, + { label: "JPY", value: "JPY" }, + ] + } + ] + + const advancedFilterFields: DataTableAdvancedFilterField<VendorPO>[] = [ + { + id: "contractNo", + label: "PO/계약번호", + type: "text", + }, + { + id: "contractName", + label: "계약명/자재내역", + type: "text", + }, + { + id: "projectName", + label: "프로젝트", + type: "text", + }, + { + id: "purchaseManager", + label: "구매/계약담당", + type: "text", + }, + { + id: "poReceiveDate", + label: "PO/계약수신일", + type: "date", + }, + { + id: "contractDate", + label: "계약체결일", + type: "date", + }, + { + id: "lastModifiedDate", + label: "최종수정일", + type: "date", + }, + ] + + const { table } = useDataTable({ + data: data.data, + columns, + pageCount: data.pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "lastModifiedDate", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable + table={table} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <ShiVendorPOToolbarActions + table={table} + selectedRows={selectedRows} + onAction={handleAction} + onViewItems={(po) => { + setSelectedPO(po) + setItemsDialogOpen(true) + }} + /> + </DataTableAdvancedToolbar> + </DataTable> + + <VendorPOItemsDialog + open={itemsDialogOpen} + onOpenChange={setItemsDialogOpen} + po={selectedPO} + /> + + {/* 서명 요청 모달 */} + {selectedPO && ( + <SignatureRequestModal + contract={{ + id: selectedPO.id, + contractNo: selectedPO.contractNo, + contractName: selectedPO.contractName, + // 필요한 다른 필드들 매핑 + } as any} + open={signatureModalOpen} + onOpenChange={setSignatureModalOpen} + onSubmit={handleSignatureRequest} + /> + )} + </> + ) +} diff --git a/lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx b/lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx new file mode 100644 index 00000000..a18c02da --- /dev/null +++ b/lib/po/vendor-table/shi-vendor-po-toolbar-actions.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, RefreshCcw } from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { VendorPO } from "./types" + +interface ShiVendorPOToolbarActionsProps { + table: Table<VendorPO> + selectedRows: number[] + onAction: (poId: number, action: string) => Promise<void> + onViewItems: (po: VendorPO) => void +} + +export function ShiVendorPOToolbarActions({ + table, + selectedRows, + onAction, + onViewItems +}: ShiVendorPOToolbarActionsProps) { + + const handleRefresh = async () => { + try { + toast.success("데이터를 새로고침했습니다.") + // TODO: 실제 데이터 새로고침 로직 추가 + window.location.reload() + } catch (error) { + toast.error("데이터 새로고침 중 오류가 발생했습니다.") + } + } + + return ( + <div className="flex items-center gap-2"> + {/* Refresh 버튼 */} + <Button + variant="samsung" + size="sm" + className="gap-2" + onClick={handleRefresh} + > + <RefreshCcw className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Refresh</span> + </Button> + + {/* Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "vendor-po-list", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> + </div> + ) +} diff --git a/lib/po/vendor-table/types.ts b/lib/po/vendor-table/types.ts new file mode 100644 index 00000000..97572ffc --- /dev/null +++ b/lib/po/vendor-table/types.ts @@ -0,0 +1,119 @@ +/** + * 벤더용 PO 데이터 타입 정의 + */ + +export interface VendorPO { + id: number + // 선택 체크박스 (1개만 선택 가능) + + // 메인 테이블 정보 + contractNo: string // PO/계약번호 (EBELN) + revision: string // Rev. (mock 데이터용) + itemNo: string // 품번 (mock 데이터용) + contractStatus: string // 계약상태 (ZPO_CNFM_STAT) + contractType: string // 계약종류 (BSART) + details: string // 상세 (mock 데이터용) + projectName: string // 프로젝트 + contractName: string // 계약명/자재내역 (ZTITLE) + contractPeriod: string // PO/계약기간 + contractQuantity: string // PO/계약수량 + currency: string // 계약통화 (ZPO_CURR) + paymentTerms: string // 지불조건 (ZTERM) + tax: string // Tax + exchangeRate: string // 환율 (WKURS) + deliveryTerms: string // 인도조건 (INCO1) + purchaseManager: string // 구매/계약담당 + poReceiveDate: string // PO/계약수신일 (AEDAT) + contractDate: string // 계약체결일 + lcNo?: string // L/C No. + priceIndexTarget: boolean // 납품대금 연동제 대상 (ZDLV_PRICE_T) + linkedContractNo?: string // 연계 PO/계약번호 + lastModifiedDate: string // 최종수정일 + lastModifiedBy: string // 최종수정자 + + // SAP ECC 추가 필드들 + poVersion?: number // PO 버전 (ZPO_VER) + purchaseDocType?: string // 구매문서유형 (BSART) + purchaseOrg?: string // 구매조직 (EKORG) + purchaseGroup?: string // 구매그룹 (EKGRP) + poConfirmStatus?: string // PO확인상태 (ZPO_CNFM_STAT) + + // 계약/보증 관련 + contractGuaranteeCode?: string // 계약보증코드 (ZCNRT_GRNT_CD) + defectGuaranteeCode?: string // 하자보증코드 (ZDFCT_GRNT_CD) + guaranteePeriodCode?: string // 보증기간코드 (ZGRNT_PRD_CD) + advancePaymentYn?: string // 선급금여부 (ZPAMT_YN) + + // 금액 관련 + budgetAmount?: number // 예산금액 (ZBGT_AMT) + budgetCurrency?: string // 예산통화 (ZBGT_CURR) + totalAmount?: number // 총 계약 금액 (ZPO_AMT) + totalAmountKrw?: number // 발주금액KRW (ZPO_AMT_KRW) + + // 전자계약/승인 관련 + electronicContractYn?: string // 전자계약필요여부 (ZELC_CNRT_ND_YN) + electronicApprovalDate?: string // 전자승인일자 (ZELC_AGR_DT) + electronicApprovalTime?: string // 전자승인시간 (ZELC_AGR_TM) + ownerApprovalYn?: string // 선주승인필요여부 (ZOWN_AGR_IND_YN) + + // 기타 + plannedInOutFlag?: string // 계획내외구분 (ZPLN_INO_GB) + settlementStandard?: string // 정산기준 (ZECAL_BSE) + weightSettlementFlag?: string // 중량정산구분 (ZWGT_ECAL_GB) + + // 연동제 관련 + priceIndexYn?: string // 납품대금연동제대상여부 (ZDLV_PRICE_T) + writtenContractNo?: string // 서면계약번호 (ZWEBELN) + contractVersion?: number // 서면계약차수 (ZVER_NO) + + // 상세품목 정보 (다이얼로그에서 표시) + items?: VendorPOItem[] +} + +export interface VendorPOItem { + contractNo: string // PO/계약번호 + itemNo: string // 품번 + prNo: string // P/R번호 + materialGroup: string // 자재그룹(명) + priceStandard: string // 단가기준 + materialNo: string // 자재번호 + itemDescription: string // 품목/자재내역 + materialSpec: string // 자재내역사양 + designMaterialNo?: string // 설계자재번호 + fittingNo?: string // Fitting No. + cert?: string // Cert. + material?: string // 재질 + specification: string // 규격 + quantity: number // 수량 + quantityUnit: string // 수량단위 + weight?: number // 중량 + weightUnit?: string // 중량단위 + totalWeight?: number // 총중량 + unitPrice: number // 단가기준 (단가) + priceUnit: string // 단가단위 + priceUnitValue: string // 가격단위값 + contractAmount: number // PO계약금액 + adjustmentAmount?: number // 조정금액 + deliveryDate: string // 납기일자 + vatType: string // VAT구분 + steelSpec?: string // 철의장 SPEC + prManager: string // P/R 담당자 +} + +// 파싱된 벤더 PO 타입 (JSON 필드들이 파싱된 상태) +export interface VendorPOParsed extends Omit<VendorPO, 'items'> { + items: VendorPOItem[] +} + +// 벤더 PO 액션 타입들 +export type VendorPOActionType = + | "pcr-create" // PCR생성 + | "item-status" // 상세품목현황 + | "contract-detail" // 계약상세 + | "po-note" // PO Note + | "price-index" // 연동표입력 + | "approve" // 승인 + | "cancel-approve" // 승인취소 + | "reject-contract" // 계약거절 + | "print-contract" // 계약서출력 + | "view-items" // 상세품목 보기 (다이얼로그) diff --git a/lib/po/vendor-table/validations.ts b/lib/po/vendor-table/validations.ts new file mode 100644 index 00000000..70b3d87a --- /dev/null +++ b/lib/po/vendor-table/validations.ts @@ -0,0 +1,58 @@ +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from "nuqs/server" +import * as z from "zod" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { VendorPO } from "./types" + +export const vendorPoSearchParamsCache = createSearchParamsCache({ + // UI 모드나 플래그 관련 + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), + + // 페이징 + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + + // 정렬 (최종수정일 기준 내림차순) + sort: getSortingStateParser<VendorPO>().withDefault([ + { id: "lastModifiedDate", desc: true }, + ]), + + // 벤더 PO 관련 필드 + contractNo: parseAsString.withDefault(""), // PO/계약번호 + contractName: parseAsString.withDefault(""), // 계약명/자재내역 + projectName: parseAsString.withDefault(""), // 프로젝트 + contractStatus: parseAsString.withDefault(""), // 계약상태 + contractType: parseAsString.withDefault(""), // 계약종류 + currency: parseAsString.withDefault(""), // 계약통화 + paymentTerms: parseAsString.withDefault(""), // 지불조건 + deliveryTerms: parseAsString.withDefault(""), // 인도조건 + purchaseManager: parseAsString.withDefault(""), // 구매/계약담당 + + // 날짜 관련 + poReceiveDate: parseAsString.withDefault(""), // PO/계약수신일 + contractDate: parseAsString.withDefault(""), // 계약체결일 + lastModifiedDate: parseAsString.withDefault(""), // 최종수정일 + + // 기타 + lcNo: parseAsString.withDefault(""), // L/C No. + priceIndexTarget: parseAsStringEnum(["true", "false"]).withDefault(""), // 납품대금 연동제 대상 + linkedContractNo: parseAsString.withDefault(""), // 연계 PO/계약번호 + lastModifiedBy: parseAsString.withDefault(""), // 최종수정자 + + // 벤더 필터링 (내부적으로 사용) + vendorId: parseAsInteger.withDefault(0), // 특정 벤더의 PO만 조회 + + // 고급 필터(Advanced) & 검색 + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), +}) + +// 최종 타입 +export type GetVendorPOSchema = Awaited<ReturnType<typeof vendorPoSearchParamsCache.parse>> diff --git a/lib/po/vendor-table/vendor-po-columns.tsx b/lib/po/vendor-table/vendor-po-columns.tsx new file mode 100644 index 00000000..1a655b0c --- /dev/null +++ b/lib/po/vendor-table/vendor-po-columns.tsx @@ -0,0 +1,511 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { + FileTextIcon, + MoreHorizontalIcon, + EyeIcon, + PrinterIcon, + FileXIcon, + PlusIcon, + EditIcon +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Checkbox } from "@/components/ui/checkbox" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { VendorPO, VendorPOActionType } from "./types" + +// 벤더 PO용 행 액션 타입 +type VendorPORowAction = { + row: { original: VendorPO } + type: VendorPOActionType +} + +interface GetVendorColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<VendorPORowAction | null>> + selectedRows?: number[] + onRowSelect?: (id: number, selected: boolean) => void +} + +export function getVendorColumns({ setRowAction, selectedRows = [], onRowSelect }: GetVendorColumnsProps): ColumnDef<VendorPO>[] { + return [ + // 선택 체크박스 (1개만 선택 가능) + { + id: "select", + header: () => <div className="text-center">선택</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Checkbox + checked={selectedRows.includes(row.original.id)} + onCheckedChange={(checked) => { + if (onRowSelect) { + onRowSelect(row.original.id, !!checked) + } + }} + aria-label="행 선택" + /> + </div> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + + // No. (ID) + { + accessorKey: "id", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="No." /> + ), + cell: ({ row }) => { + const id = row.getValue("id") as number + return <div className="text-sm font-mono">{id}</div> + }, + size: 60, + }, + + // PO/계약번호 + { + accessorKey: "contractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약번호" /> + ), + cell: ({ row }) => { + const contractNo = row.getValue("contractNo") as string + return ( + <div className="font-medium"> + {contractNo} + </div> + ) + }, + size: 120, + }, + + // Rev. / 품번 (PO 버전) + { + accessorKey: "poVersion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Rev. / 품번" /> + ), + cell: ({ row }) => { + const version = row.getValue("poVersion") as number + return <div className="text-sm font-medium">{version || '-'}</div> + }, + size: 80, + }, + + // 계약상태 + { + accessorKey: "contractStatus", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약상태" /> + ), + cell: ({ row }) => { + const status = row.getValue("contractStatus") as string + return ( + <Badge variant="outline"> + {status || '-'} + </Badge> + ) + }, + size: 100, + }, + + // 계약종류 + { + accessorKey: "contractType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약종류" /> + ), + cell: ({ row }) => { + const type = row.getValue("contractType") as string + return <Badge variant="outline">{type || '-'}</Badge> + }, + size: 100, + }, + + // 상세품목 (버튼) + { + id: "itemsAction", + header: () => <div className="text-center">상세품목</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="h-3.5 w-3.5 mr-1" /> + 보기 + </Button> + </div> + ), + enableSorting: false, + size: 80, + }, + + // 프로젝트 + { + accessorKey: "projectName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + ), + cell: ({ row }) => { + const projectName = row.getValue("projectName") as string + return ( + <div className="max-w-[150px] truncate" title={projectName}> + {projectName || '-'} + </div> + ) + }, + size: 150, + }, + + // 계약명/자재내역 + { + accessorKey: "contractName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약명/자재내역" /> + ), + cell: ({ row }) => { + const contractName = row.getValue("contractName") as string + return ( + <div className="max-w-[200px] truncate" title={contractName}> + {contractName || '-'} + </div> + ) + }, + size: 200, + }, + + // PO/계약기간 + { + accessorKey: "contractPeriod", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약기간" /> + ), + cell: ({ row }) => { + const period = row.getValue("contractPeriod") as string + return ( + <div className="text-sm whitespace-nowrap"> + {period || '-'} + </div> + ) + }, + size: 150, + }, + + // PO/계약금액 + { + accessorKey: "totalAmount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약금액" /> + ), + cell: ({ row }) => { + const amount = row.getValue("totalAmount") as string | number + return <div className="text-sm text-right font-mono">{amount || '-'}</div> + }, + size: 120, + }, + + // 계약통화 + { + accessorKey: "currency", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약통화" /> + ), + cell: ({ row }) => { + const currency = row.getValue("currency") as string + return <div className="text-sm font-mono">{currency || '-'}</div> + }, + size: 80, + }, + + // 지불조건 + { + accessorKey: "paymentTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="지불조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("paymentTerms") as string + return ( + <div className="max-w-[120px] truncate text-sm" title={terms}> + {terms || '-'} + </div> + ) + }, + size: 120, + }, + + // Tax + { + accessorKey: "tax", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Tax" /> + ), + cell: ({ row }) => { + const tax = row.getValue("tax") as string + return <div className="text-sm">{tax || '-'}</div> + }, + size: 80, + }, + + // 환율 + { + accessorKey: "exchangeRate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="환율" /> + ), + cell: ({ row }) => { + const rate = row.getValue("exchangeRate") as string + return <div className="text-sm font-mono">{rate || '-'}</div> + }, + size: 100, + }, + + // 인도조건 + { + accessorKey: "deliveryTerms", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="인도조건" /> + ), + cell: ({ row }) => { + const terms = row.getValue("deliveryTerms") as string + return <div className="text-sm">{terms || '-'}</div> + }, + size: 100, + }, + + // 구매/계약담당 + { + accessorKey: "purchaseManager", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="구매/계약담당" /> + ), + cell: ({ row }) => { + const manager = row.getValue("purchaseManager") as string + return <div className="text-sm">{manager || '-'}</div> + }, + size: 120, + }, + + // PO/계약수신일 + { + accessorKey: "poReceiveDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PO/계약수신일" /> + ), + cell: ({ row }) => { + const date = row.getValue("poReceiveDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 계약체결일 + { + accessorKey: "contractDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="계약체결일" /> + ), + cell: ({ row }) => { + const date = row.getValue("contractDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // L/C No. + { + accessorKey: "lcNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="L/C No." /> + ), + cell: ({ row }) => { + const lcNo = row.getValue("lcNo") as string + return <div className="text-sm">{lcNo || '-'}</div> + }, + size: 120, + }, + + // 납품대금 연동제 대상 + { + accessorKey: "priceIndexTarget", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="납품대금 연동제 대상" /> + ), + cell: ({ row }) => { + const target = row.getValue("priceIndexTarget") as string | boolean + return <div className="text-sm">{target?.toString() || '-'}</div> + }, + size: 140, + }, + + // 연계 PO/계약번호 + { + accessorKey: "linkedContractNo", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="연계 PO/계약번호" /> + ), + cell: ({ row }) => { + const linkedNo = row.getValue("linkedContractNo") as string + return <div className="text-sm">{linkedNo || '-'}</div> + }, + size: 140, + }, + + // 최종수정일 + { + accessorKey: "lastModifiedDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정일" /> + ), + cell: ({ row }) => { + const date = row.getValue("lastModifiedDate") as string + return <div className="text-sm">{date || '-'}</div> + }, + size: 120, + }, + + // 최종수정자 + { + accessorKey: "lastModifiedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종수정자" /> + ), + cell: ({ row }) => { + const user = row.getValue("lastModifiedBy") as string + return <div className="text-sm">{user || '-'}</div> + }, + size: 120, + }, + + // 액션 버튼들 + { + id: "actions", + enableHiding: false, + header: () => <div className="text-center">액션</div>, + cell: function Cell({ row }) { + return ( + <div className="flex gap-1"> + {/* 상세품목 버튼 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 px-2" + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="h-3.5 w-3.5" aria-hidden="true" /> + </Button> + </TooltipTrigger> + <TooltipContent> + 상세품목 보기 + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 드롭다운 메뉴 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <span className="sr-only">Open menu</span> + <MoreHorizontalIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>액션</DropdownMenuLabel> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "view-items" })} + > + <FileTextIcon className="mr-2 h-4 w-4" /> + 상세품목 보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "pcr-create" })} + > + <PlusIcon className="mr-2 h-4 w-4" /> + PCR생성 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "approve" })} + > + 승인 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "cancel-approve" })} + > + 승인취소 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "reject-contract" })} + className="text-red-600" + > + <FileXIcon className="mr-2 h-4 w-4" /> + 계약거절 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "print-contract" })} + > + <PrinterIcon className="mr-2 h-4 w-4" /> + 계약서출력 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "contract-detail" })} + > + <EyeIcon className="mr-2 h-4 w-4" /> + 계약상세 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "po-note" })} + > + <EditIcon className="mr-2 h-4 w-4" /> + PO Note + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => setRowAction({ row, type: "price-index" })} + > + 연동표입력 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </div> + ); + }, + size: 120, + minSize: 100, + }, + ] +}
\ No newline at end of file diff --git a/lib/po/vendor-table/vendor-po-items-dialog.tsx b/lib/po/vendor-table/vendor-po-items-dialog.tsx new file mode 100644 index 00000000..d3b33371 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-items-dialog.tsx @@ -0,0 +1,199 @@ +"use client" + +import * as React from "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 { Skeleton } from "@/components/ui/skeleton" +import { VendorPO, VendorPOItem } from "./types" +import { getVendorPOItemsByContractNo } from "./service" + +interface VendorPOItemsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + po: VendorPO | null +} + +export function VendorPOItemsDialog({ open, onOpenChange, po }: VendorPOItemsDialogProps) { + const [items, setItems] = React.useState<VendorPOItem[]>([]) + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState<string | null>(null) + + // 상세품목 데이터 로드 + React.useEffect(() => { + if (!open || !po) { + setItems([]) + setError(null) + return + } + + const loadItems = async () => { + setLoading(true) + setError(null) + try { + const vendorPOItems = await getVendorPOItemsByContractNo(po.contractNo) + setItems(vendorPOItems) + } catch (err) { + console.error("Failed to load vendor PO items:", err) + setError("상세품목을 불러오는 중 오류가 발생했습니다.") + } finally { + setLoading(false) + } + } + + loadItems() + }, [open, po]) + + if (!po) return null + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-[95vw] max-h-[90vh] w-full flex flex-col"> + <DialogHeader className="flex-shrink-0"> + <DialogTitle className="text-lg font-semibold"> + 상세품목 현황 - {po.contractNo} + </DialogTitle> + <DialogDescription> + {po.contractName} ({items.length}개 품목) + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-hidden"> + {loading ? ( + <div className="space-y-4"> + <div className="text-center py-4 text-muted-foreground"> + 상세품목을 불러오는 중... + </div> + <div className="space-y-2"> + {Array.from({ length: 3 }).map((_, i) => ( + <Skeleton key={i} className="h-12 w-full" /> + ))} + </div> + </div> + ) : error ? ( + <div className="text-center py-8 text-destructive"> + {error} + </div> + ) : items.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + 등록된 상세품목이 없습니다. + </div> + ) : ( + <div className="overflow-auto max-h-[60vh]"> + <Table> + <TableHeader className="sticky top-0 bg-background z-10"> + <TableRow> + <TableHead className="min-w-[120px] whitespace-nowrap">PO/계약번호</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">품번</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">P/R번호</TableHead> + <TableHead className="min-w-[120px] whitespace-nowrap">자재그룹(명)</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">단가기준</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">자재번호</TableHead> + <TableHead className="min-w-[200px] whitespace-nowrap">품목/자재내역</TableHead> + <TableHead className="min-w-[200px] whitespace-nowrap">자재내역사양</TableHead> + <TableHead className="min-w-[120px] whitespace-nowrap">설계자재번호</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">Fitting No.</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">Cert.</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">재질</TableHead> + <TableHead className="min-w-[150px] whitespace-nowrap">규격</TableHead> + <TableHead className="min-w-[80px] text-right whitespace-nowrap">수량</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">수량단위</TableHead> + <TableHead className="min-w-[80px] text-right whitespace-nowrap">중량</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">중량단위</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">총중량</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">단가기준</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">단가단위</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">가격단위값</TableHead> + <TableHead className="min-w-[120px] text-right whitespace-nowrap">PO계약금액</TableHead> + <TableHead className="min-w-[100px] text-right whitespace-nowrap">조정금액</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">납기일자</TableHead> + <TableHead className="min-w-[80px] whitespace-nowrap">VAT구분</TableHead> + <TableHead className="min-w-[120px] whitespace-nowrap">철의장 SPEC</TableHead> + <TableHead className="min-w-[100px] whitespace-nowrap">P/R 담당자</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {items.map((item, index) => ( + <TableRow key={`${item.contractNo}-${item.itemNo}-${index}`}> + <TableCell className="font-medium">{item.contractNo || '-'}</TableCell> + <TableCell>{item.itemNo || '-'}</TableCell> + <TableCell>{item.prNo || '-'}</TableCell> + <TableCell>{item.materialGroup || '-'}</TableCell> + <TableCell>{item.priceStandard || '-'}</TableCell> + <TableCell className="font-mono text-sm">{item.materialNo || '-'}</TableCell> + <TableCell className="max-w-[200px]"> + <div className="truncate" title={item.itemDescription || ''}> + {item.itemDescription || '-'} + </div> + </TableCell> + <TableCell className="max-w-[200px]"> + <div className="truncate" title={item.materialSpec || ''}> + {item.materialSpec || '-'} + </div> + </TableCell> + <TableCell>{item.designMaterialNo || '-'}</TableCell> + <TableCell>{item.fittingNo || '-'}</TableCell> + <TableCell>{item.cert || '-'}</TableCell> + <TableCell>{item.material || '-'}</TableCell> + <TableCell>{item.specification || '-'}</TableCell> + <TableCell className="text-right font-mono"> + {item.quantity?.toLocaleString() || '-'} + </TableCell> + <TableCell>{item.quantityUnit || '-'}</TableCell> + <TableCell className="text-right font-mono"> + {item.weight ? item.weight.toLocaleString() : '-'} + </TableCell> + <TableCell>{item.weightUnit || '-'}</TableCell> + <TableCell className="text-right font-mono"> + {item.totalWeight ? item.totalWeight.toLocaleString() : '-'} + </TableCell> + <TableCell className="text-right font-mono"> + {item.unitPrice?.toLocaleString() || '-'} + </TableCell> + <TableCell>{item.priceUnit || '-'}</TableCell> + <TableCell>{item.priceUnitValue || '-'}</TableCell> + <TableCell className="text-right font-mono font-semibold"> + {item.contractAmount?.toLocaleString() || '-'} + </TableCell> + <TableCell className="text-right font-mono"> + {item.adjustmentAmount ? item.adjustmentAmount.toLocaleString() : '-'} + </TableCell> + <TableCell>{item.deliveryDate || '-'}</TableCell> + <TableCell>{item.vatType || '-'}</TableCell> + <TableCell>{item.steelSpec || '-'}</TableCell> + <TableCell>{item.prManager || '-'}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + </div> + + {items.length > 0 && ( + <div className="flex justify-between items-center pt-4 border-t flex-shrink-0"> + <div className="text-sm text-muted-foreground"> + 총 {items.length}개 품목 + </div> + <div className="text-sm font-medium"> + 총 계약금액: {items.reduce((sum, item) => sum + item.contractAmount, 0).toLocaleString()} 원 + </div> + </div> + )} + </DialogContent> + </Dialog> + ) +} diff --git a/lib/po/vendor-table/vendor-po-table.tsx b/lib/po/vendor-table/vendor-po-table.tsx new file mode 100644 index 00000000..a3ad4949 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-table.tsx @@ -0,0 +1,247 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { toast } from "sonner" + +import { getVendorPOs, handleVendorPOAction } from "./service" +import { getVendorColumns } from "./vendor-po-columns" +import { VendorPO, VendorPOActionType } from "./types" +import { VendorPOItemsDialog } from "./vendor-po-items-dialog" +import { VendorPOToolbarActions } from "./vendor-po-toolbar-actions" + +interface VendorPoTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getVendorPOs>>, + ] + > +} + +export function VendorPoTable({ promises }: VendorPoTableProps) { + const [data, setData] = React.useState<{ + data: VendorPO[]; + pageCount: number; + }>({ data: [], pageCount: 0 }); + + const [selectedRows, setSelectedRows] = React.useState<number[]>([]) + + // 데이터 로딩 + React.useEffect(() => { + promises.then(([result]) => { + console.log("Vendor PO data:", result.data) + setData(result); + }); + }, [promises]); + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<VendorPO> | null>(null) + + // 다이얼로그 상태 + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [selectedPO, setSelectedPO] = React.useState<VendorPO | null>(null) + + // 행 선택 처리 (1개만 선택 가능) + const handleRowSelect = (id: number, selected: boolean) => { + if (selected) { + setSelectedRows([id]) // 1개만 선택 + } else { + setSelectedRows([]) + } + } + + // 행 액션 처리 + React.useEffect(() => { + if (!rowAction) return + + const po = rowAction.row.original + setSelectedPO(po) + + switch (rowAction.type as VendorPOActionType) { + case "view-items": + setItemsDialogOpen(true) + break + case "pcr-create": + handleAction(po.id, "pcr-create") + break + case "approve": + handleAction(po.id, "approve") + break + case "cancel-approve": + handleAction(po.id, "cancel-approve") + break + case "reject-contract": + handleAction(po.id, "reject-contract") + break + case "print-contract": + handleAction(po.id, "print-contract") + break + case "item-status": + setItemsDialogOpen(true) + break + case "contract-detail": + toast.info("계약상세 기능은 개발 중입니다.") + break + case "po-note": + toast.info("PO Note 기능은 개발 중입니다.") + break + case "price-index": + toast.info("연동표입력 기능은 개발 중입니다.") + break + default: + toast.info("해당 기능은 개발 중입니다.") + } + + setRowAction(null) + }, [rowAction]) + + // 액션 처리 함수 + const handleAction = async (poId: number, action: string) => { + try { + const result = await handleVendorPOAction(poId, action) + if (result.success) { + toast.success(result.message) + // 필요시 데이터 새로고침 + } else { + toast.error(result.message) + } + } catch (error) { + console.error("Action error:", error) + toast.error("액션 처리 중 오류가 발생했습니다.") + } + } + + const columns = React.useMemo( + () => getVendorColumns({ + setRowAction, + selectedRows, + onRowSelect: handleRowSelect + }), + [selectedRows] + ) + + const filterFields: DataTableFilterField<VendorPO>[] = [ + { + id: "contractStatus", + label: "계약상태", + options: [ + { label: "승인대기", value: "승인대기" }, + { label: "계약완료", value: "계약완료" }, + { label: "진행중", value: "진행중" }, + { label: "수정요청", value: "수정요청" }, + { label: "거절됨", value: "거절됨" }, + ] + }, + { + id: "contractType", + label: "계약종류", + options: [ + { label: "구매계약", value: "구매계약" }, + { label: "서비스계약", value: "서비스계약" }, + { label: "임가공계약", value: "임가공계약" }, + ] + }, + { + id: "currency", + label: "계약통화", + options: [ + { label: "KRW", value: "KRW" }, + { label: "USD", value: "USD" }, + { label: "EUR", value: "EUR" }, + { label: "JPY", value: "JPY" }, + ] + } + ] + + const advancedFilterFields: DataTableAdvancedFilterField<VendorPO>[] = [ + { + id: "contractNo", + label: "PO/계약번호", + type: "text", + }, + { + id: "contractName", + label: "계약명/자재내역", + type: "text", + }, + { + id: "projectName", + label: "프로젝트", + type: "text", + }, + { + id: "purchaseManager", + label: "구매/계약담당", + type: "text", + }, + { + id: "poReceiveDate", + label: "PO/계약수신일", + type: "date", + }, + { + id: "contractDate", + label: "계약체결일", + type: "date", + }, + { + id: "lastModifiedDate", + label: "최종수정일", + type: "date", + }, + ] + + const { table } = useDataTable({ + data: data.data, + columns, + pageCount: data.pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "lastModifiedDate", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable + table={table} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <VendorPOToolbarActions + table={table} + selectedRows={selectedRows} + onAction={handleAction} + onViewItems={(po) => { + setSelectedPO(po) + setItemsDialogOpen(true) + }} + /> + </DataTableAdvancedToolbar> + </DataTable> + + <VendorPOItemsDialog + open={itemsDialogOpen} + onOpenChange={setItemsDialogOpen} + po={selectedPO} + /> + </> + ) +}
\ No newline at end of file diff --git a/lib/po/vendor-table/vendor-po-toolbar-actions.tsx b/lib/po/vendor-table/vendor-po-toolbar-actions.tsx new file mode 100644 index 00000000..800a9e40 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-toolbar-actions.tsx @@ -0,0 +1,214 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { + EyeIcon, + EditIcon, + FileXIcon, + PrinterIcon, +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { toast } from "sonner" +import { VendorPO } from "./types" + +interface VendorPOToolbarActionsProps { + table: Table<VendorPO> + selectedRows: number[] + onAction: (poId: number, action: string) => Promise<void> + onViewItems?: (po: VendorPO) => void +} + +export function VendorPOToolbarActions({ + table, + selectedRows, + onAction, + onViewItems +}: VendorPOToolbarActionsProps) { + const hasSelectedRow = selectedRows.length === 1 + const selectedPO = hasSelectedRow ? + table.getRowModel().rows.find(row => selectedRows.includes(row.original.id))?.original + : null + + const handleToolbarAction = async (action: string) => { + if (!hasSelectedRow || !selectedPO) { + toast.error("먼저 PO를 선택해주세요.") + return + } + + // view-items 액션은 특별히 처리 + if (action === "view-items") { + if (onViewItems) { + onViewItems(selectedPO) + } + return + } + + await onAction(selectedPO.id, action) + } + + return ( + <div className="flex items-center gap-2"> + {/* 주요 액션 버튼들 */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="default" + size="sm" + onClick={() => handleToolbarAction("pcr-create")} + disabled={!hasSelectedRow} + className="h-8" + > + PCR생성 + </Button> + </TooltipTrigger> + <TooltipContent>선택된 PO에 대한 PCR을 생성합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => handleToolbarAction("view-items")} + disabled={!hasSelectedRow} + className="h-8" + > + 상세품목현황 + </Button> + </TooltipTrigger> + <TooltipContent>상세품목 현황을 확인합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* 승인 관련 액션 */} + {selectedPO?.contractStatus !== "승인완료" && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => handleToolbarAction("approve")} + disabled={!hasSelectedRow} + className="h-8 text-green-600 border-green-600 hover:bg-green-50" + > + 승인 + </Button> + </TooltipTrigger> + <TooltipContent>선택된 계약을 승인합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + )} + + {selectedPO?.contractStatus === "승인완료" && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => handleToolbarAction("cancel-approve")} + disabled={!hasSelectedRow} + className="h-8 text-orange-600 border-orange-600 hover:bg-orange-50" + > + 승인취소 + </Button> + </TooltipTrigger> + <TooltipContent>승인을 취소합니다</TooltipContent> + </Tooltip> + </TooltipProvider> + )} + + {/* 더 많은 액션 드롭다운 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="outline" + size="sm" + disabled={!hasSelectedRow} + className="h-8" + > + 더 많은 액션 + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-[200px]"> + <DropdownMenuLabel>계약 관련</DropdownMenuLabel> + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => handleToolbarAction("contract-detail")} + disabled={!hasSelectedRow} + > + <EyeIcon className="mr-2 h-4 w-4" /> + 계약상세 + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => handleToolbarAction("po-note")} + disabled={!hasSelectedRow} + > + <EditIcon className="mr-2 h-4 w-4" /> + PO Note + </DropdownMenuItem> + + <DropdownMenuItem + onClick={() => handleToolbarAction("price-index")} + disabled={!hasSelectedRow} + > + <EditIcon className="mr-2 h-4 w-4" /> + 연동표입력 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => handleToolbarAction("reject-contract")} + disabled={!hasSelectedRow} + className="text-red-600 focus:text-red-600" + > + <FileXIcon className="mr-2 h-4 w-4" /> + 계약거절 + </DropdownMenuItem> + + <DropdownMenuSeparator /> + + <DropdownMenuItem + onClick={() => handleToolbarAction("print-contract")} + disabled={!hasSelectedRow} + > + <PrinterIcon className="mr-2 h-4 w-4" /> + 계약서출력 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* 선택된 행 정보 표시 + {hasSelectedRow && selectedPO && ( + <div className="flex items-center gap-2 ml-4 text-sm text-muted-foreground"> + <span>선택됨:</span> + <span className="font-medium">{selectedPO.contractNo}</span> + <span>({selectedPO.contractName})</span> + </div> + )} */} + </div> + ) +} |
