Merge branch 'jskim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
@@ -616,6 +616,7 @@ export default function PurchaseItemPage() {
|
|||||||
page: 1, size: 500,
|
page: 1, size: 500,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
||||||
autoFilter: true,
|
autoFilter: true,
|
||||||
|
sort: { columnName: "created_date", order: "asc" },
|
||||||
});
|
});
|
||||||
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||||
|
|
||||||
@@ -866,6 +867,7 @@ export default function PurchaseItemPage() {
|
|||||||
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
||||||
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||||
]}, autoFilter: true,
|
]}, autoFilter: true,
|
||||||
|
sort: { columnName: "created_date", order: "asc" },
|
||||||
});
|
});
|
||||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||||
mappingRows = allMappings
|
mappingRows = allMappings
|
||||||
@@ -887,7 +889,8 @@ export default function PurchaseItemPage() {
|
|||||||
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||||
]}, autoFilter: true,
|
]}, autoFilter: true,
|
||||||
});
|
});
|
||||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||||
|
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||||
priceRows = allPriceData.map((p: any) => ({
|
priceRows = allPriceData.map((p: any) => ({
|
||||||
_id: `p_existing_${p.id}`,
|
_id: `p_existing_${p.id}`,
|
||||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||||
@@ -928,104 +931,104 @@ export default function PurchaseItemPage() {
|
|||||||
const mappingRows = suppMappings[custKey] || [];
|
const mappingRows = suppMappings[custKey] || [];
|
||||||
|
|
||||||
if (isEditingExisting && editSuppData?.id) {
|
if (isEditingExisting && editSuppData?.id) {
|
||||||
// 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
// 기존 매핑 조회
|
||||||
const keptIds = new Set<string>();
|
let existingMaps: any[] = [];
|
||||||
let firstMappingId: string | null = null;
|
try {
|
||||||
|
const existingMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||||
|
page: 1, size: 100,
|
||||||
|
dataFilter: { enabled: true, filters: [
|
||||||
|
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
||||||
|
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
||||||
|
]}, autoFilter: true,
|
||||||
|
sort: { columnName: "created_date", order: "asc" },
|
||||||
|
});
|
||||||
|
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
|
// 매핑 upsert: 인덱스 기반
|
||||||
|
const usedExistingIds = new Set<string>();
|
||||||
|
let firstMappingId: string | null = editSuppData.id;
|
||||||
for (let mi = 0; mi < mappingRows.length; mi++) {
|
for (let mi = 0; mi < mappingRows.length; mi++) {
|
||||||
const m = mappingRows[mi];
|
const existMap = existingMaps[mi];
|
||||||
if (m._id?.startsWith("m_existing_")) {
|
if (existMap) {
|
||||||
const realId = m._id.replace("m_existing_", "");
|
|
||||||
keptIds.add(realId);
|
|
||||||
if (mi === 0) firstMappingId = realId;
|
|
||||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||||
originalData: { id: realId },
|
originalData: { id: existMap.id },
|
||||||
updatedData: {
|
updatedData: {
|
||||||
supplier_item_code: m.supplier_item_code || "",
|
supplier_item_code: mappingRows[mi].supplier_item_code || "",
|
||||||
supplier_item_name: m.supplier_item_name || "",
|
supplier_item_name: mappingRows[mi].supplier_item_name || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
usedExistingIds.add(existMap.id);
|
||||||
|
if (mi === 0) firstMappingId = existMap.id;
|
||||||
} else {
|
} else {
|
||||||
const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
const mRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
supplier_id: custKey, item_id: selectedItem.item_number,
|
supplier_id: custKey, item_id: selectedItem.item_number,
|
||||||
supplier_item_code: m.supplier_item_code || "",
|
supplier_item_code: mappingRows[mi].supplier_item_code || "",
|
||||||
supplier_item_name: m.supplier_item_name || "",
|
supplier_item_name: mappingRows[mi].supplier_item_name || "",
|
||||||
});
|
});
|
||||||
if (mi === 0) firstMappingId = res.data?.data?.id || null;
|
if (mi === 0 && !firstMappingId) firstMappingId = mRes.data?.data?.id || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 폼에서 삭제된 매핑 → DB 삭제
|
// 초과분 delete
|
||||||
|
const toDeleteMaps = existingMaps.filter((m) => !usedExistingIds.has(m.id));
|
||||||
|
if (toDeleteMaps.length > 0) {
|
||||||
|
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||||
|
data: toDeleteMaps.map((m: any) => ({ id: m.id })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 단가 조회
|
||||||
|
let existingPriceRows: any[] = [];
|
||||||
try {
|
try {
|
||||||
const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
const existingPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
|
||||||
page: 1, size: 100,
|
page: 1, size: 100,
|
||||||
dataFilter: { enabled: true, filters: [
|
dataFilter: { enabled: true, filters: [
|
||||||
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
||||||
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
||||||
]}, autoFilter: true,
|
]}, autoFilter: true,
|
||||||
});
|
});
|
||||||
const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || [])
|
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||||
.filter((m: any) => !keptIds.has(m.id));
|
|
||||||
if (toDelete.length > 0) {
|
|
||||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
|
||||||
data: toDelete.map((m: any) => ({ id: m.id })),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
|
|
||||||
if (!firstMappingId) firstMappingId = editSuppData.id;
|
// 단가 upsert: 인덱스 기반
|
||||||
|
|
||||||
// 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
|
||||||
const keptPriceIds = new Set<string>();
|
|
||||||
const priceRows = (suppPrices[custKey] || []).filter((p) =>
|
const priceRows = (suppPrices[custKey] || []).filter((p) =>
|
||||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||||
);
|
);
|
||||||
for (const price of priceRows) {
|
const usedPriceIds = new Set<string>();
|
||||||
if (price._id?.startsWith("p_existing_")) {
|
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||||
const realId = price._id.replace("p_existing_", "");
|
const price = priceRows[pi];
|
||||||
keptPriceIds.add(realId);
|
const priceData = {
|
||||||
|
mapping_id: firstMappingId || editSuppData.id,
|
||||||
|
supplier_id: custKey, item_id: selectedItem.item_number,
|
||||||
|
start_date: price.start_date || null, end_date: price.end_date || null,
|
||||||
|
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
||||||
|
base_price: price.base_price ? Number(price.base_price) : null,
|
||||||
|
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
|
||||||
|
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
||||||
|
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
||||||
|
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
||||||
|
};
|
||||||
|
const existPrice = existingPriceRows[pi];
|
||||||
|
if (existPrice) {
|
||||||
await apiClient.put(`/table-management/tables/supplier_item_prices/edit`, {
|
await apiClient.put(`/table-management/tables/supplier_item_prices/edit`, {
|
||||||
originalData: { id: realId },
|
originalData: { id: existPrice.id },
|
||||||
updatedData: {
|
updatedData: priceData,
|
||||||
mapping_id: firstMappingId || "",
|
|
||||||
start_date: price.start_date || null, end_date: price.end_date || null,
|
|
||||||
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
|
||||||
base_price: price.base_price ? Number(price.base_price) : null,
|
|
||||||
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
|
||||||
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
|
||||||
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
usedPriceIds.add(existPrice.id);
|
||||||
} else {
|
} else {
|
||||||
await apiClient.post(`/table-management/tables/supplier_item_prices/add`, {
|
await apiClient.post(`/table-management/tables/supplier_item_prices/add`, {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(), ...priceData,
|
||||||
mapping_id: firstMappingId || "",
|
|
||||||
supplier_id: custKey, item_id: selectedItem.item_number,
|
|
||||||
start_date: price.start_date || null, end_date: price.end_date || null,
|
|
||||||
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
|
||||||
base_price: price.base_price ? Number(price.base_price) : null,
|
|
||||||
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
|
||||||
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
|
||||||
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 폼에서 삭제된 단가 → DB 삭제
|
// 초과분 delete
|
||||||
try {
|
const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.has(p.id));
|
||||||
const dbPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
|
if (toDeletePrices.length > 0) {
|
||||||
page: 1, size: 100,
|
await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, {
|
||||||
dataFilter: { enabled: true, filters: [
|
data: toDeletePrices.map((p: any) => ({ id: p.id })),
|
||||||
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
|
||||||
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
|
||||||
]}, autoFilter: true,
|
|
||||||
});
|
});
|
||||||
const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || [])
|
}
|
||||||
.filter((p: any) => !keptPriceIds.has(p.id));
|
|
||||||
if (toDeletePrices.length > 0) {
|
|
||||||
await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, {
|
|
||||||
data: toDeletePrices.map((p: any) => ({ id: p.id })),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch { /* skip */ }
|
|
||||||
} else {
|
} else {
|
||||||
// 신규 등록
|
// 신규 등록
|
||||||
const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
||||||
@@ -1055,6 +1058,7 @@ export default function PurchaseItemPage() {
|
|||||||
start_date: price.start_date || null, end_date: price.end_date || null,
|
start_date: price.start_date || null, end_date: price.end_date || null,
|
||||||
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
||||||
base_price: price.base_price ? Number(price.base_price) : null,
|
base_price: price.base_price ? Number(price.base_price) : null,
|
||||||
|
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
|
||||||
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
||||||
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
||||||
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
||||||
@@ -1719,7 +1723,7 @@ export default function PurchaseItemPage() {
|
|||||||
|
|
||||||
{/* ── 공급업체 상세 입력/수정 모달 ── */}
|
{/* ── 공급업체 상세 입력/수정 모달 ── */}
|
||||||
<Dialog open={suppDetailOpen} onOpenChange={setSuppDetailOpen}>
|
<Dialog open={suppDetailOpen} onOpenChange={setSuppDetailOpen}>
|
||||||
<DialogContent className="max-w-[1100px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-[1100px] h-[85vh] flex flex-col overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
공급업체 상세정보 {editSuppData ? "수정" : "입력"} — {selectedItem?.item_name || ""}
|
공급업체 상세정보 {editSuppData ? "수정" : "입력"} — {selectedItem?.item_name || ""}
|
||||||
@@ -1729,7 +1733,7 @@ export default function PurchaseItemPage() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-6 py-2">
|
<div className="space-y-6 py-2 flex-1 overflow-y-auto">
|
||||||
{selectedSuppsForDetail.map((cust, idx) => {
|
{selectedSuppsForDetail.map((cust, idx) => {
|
||||||
const custKey = cust.supplier_code || cust.id;
|
const custKey = cust.supplier_code || cust.id;
|
||||||
const mappingRows = suppMappings[custKey] || [];
|
const mappingRows = suppMappings[custKey] || [];
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export default function SalesItemPage() {
|
|||||||
calculated_price: string;
|
calculated_price: string;
|
||||||
}>>>({});
|
}>>>({});
|
||||||
const [editCustData, setEditCustData] = useState<any>(null);
|
const [editCustData, setEditCustData] = useState<any>(null);
|
||||||
|
const [collapsedPriceCards, setCollapsedPriceCards] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
|
||||||
// 카테고리 로드
|
// 카테고리 로드
|
||||||
@@ -368,6 +369,7 @@ export default function SalesItemPage() {
|
|||||||
page: 1, size: 500,
|
page: 1, size: 500,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
||||||
autoFilter: true,
|
autoFilter: true,
|
||||||
|
sort: { columnName: "created_date", order: "asc" },
|
||||||
});
|
});
|
||||||
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||||
|
|
||||||
@@ -618,6 +620,7 @@ export default function SalesItemPage() {
|
|||||||
{ columnName: "customer_id", operator: "equals", value: custKey },
|
{ columnName: "customer_id", operator: "equals", value: custKey },
|
||||||
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||||
]}, autoFilter: true,
|
]}, autoFilter: true,
|
||||||
|
sort: { columnName: "created_date", order: "asc" },
|
||||||
});
|
});
|
||||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||||
mappingRows = allMappings
|
mappingRows = allMappings
|
||||||
@@ -639,7 +642,8 @@ export default function SalesItemPage() {
|
|||||||
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||||
]}, autoFilter: true,
|
]}, autoFilter: true,
|
||||||
});
|
});
|
||||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||||
|
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||||
priceRows = allPriceData.map((p: any) => ({
|
priceRows = allPriceData.map((p: any) => ({
|
||||||
_id: `p_existing_${p.id}`,
|
_id: `p_existing_${p.id}`,
|
||||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||||
@@ -680,104 +684,104 @@ export default function SalesItemPage() {
|
|||||||
const mappingRows = custMappings[custKey] || [];
|
const mappingRows = custMappings[custKey] || [];
|
||||||
|
|
||||||
if (isEditingExisting && editCustData?.id) {
|
if (isEditingExisting && editCustData?.id) {
|
||||||
// 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
// 기존 매핑 조회
|
||||||
const keptIds = new Set<string>();
|
let existingMaps: any[] = [];
|
||||||
let firstMappingId: string | null = null;
|
try {
|
||||||
|
const existingMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||||
|
page: 1, size: 100,
|
||||||
|
dataFilter: { enabled: true, filters: [
|
||||||
|
{ columnName: "customer_id", operator: "equals", value: custKey },
|
||||||
|
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
||||||
|
]}, autoFilter: true,
|
||||||
|
sort: { columnName: "created_date", order: "asc" },
|
||||||
|
});
|
||||||
|
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
|
// 매핑 upsert: 인덱스 기반
|
||||||
|
const usedExistingIds = new Set<string>();
|
||||||
|
let firstMappingId: string | null = editCustData.id;
|
||||||
for (let mi = 0; mi < mappingRows.length; mi++) {
|
for (let mi = 0; mi < mappingRows.length; mi++) {
|
||||||
const m = mappingRows[mi];
|
const existMap = existingMaps[mi];
|
||||||
if (m._id?.startsWith("m_existing_")) {
|
if (existMap) {
|
||||||
const realId = m._id.replace("m_existing_", "");
|
|
||||||
keptIds.add(realId);
|
|
||||||
if (mi === 0) firstMappingId = realId;
|
|
||||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||||
originalData: { id: realId },
|
originalData: { id: existMap.id },
|
||||||
updatedData: {
|
updatedData: {
|
||||||
customer_item_code: m.customer_item_code || "",
|
customer_item_code: mappingRows[mi].customer_item_code || "",
|
||||||
customer_item_name: m.customer_item_name || "",
|
customer_item_name: mappingRows[mi].customer_item_name || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
usedExistingIds.add(existMap.id);
|
||||||
|
if (mi === 0) firstMappingId = existMap.id;
|
||||||
} else {
|
} else {
|
||||||
const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
const mRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
customer_id: custKey, item_id: selectedItem.item_number,
|
customer_id: custKey, item_id: selectedItem.item_number,
|
||||||
customer_item_code: m.customer_item_code || "",
|
customer_item_code: mappingRows[mi].customer_item_code || "",
|
||||||
customer_item_name: m.customer_item_name || "",
|
customer_item_name: mappingRows[mi].customer_item_name || "",
|
||||||
});
|
});
|
||||||
if (mi === 0) firstMappingId = res.data?.data?.id || null;
|
if (mi === 0 && !firstMappingId) firstMappingId = mRes.data?.data?.id || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 폼에서 삭제된 매핑 → DB 삭제
|
// 초과분 delete
|
||||||
|
const toDeleteMaps = existingMaps.filter((m) => !usedExistingIds.has(m.id));
|
||||||
|
if (toDeleteMaps.length > 0) {
|
||||||
|
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||||
|
data: toDeleteMaps.map((m: any) => ({ id: m.id })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 단가 조회
|
||||||
|
let existingPriceRows: any[] = [];
|
||||||
try {
|
try {
|
||||||
const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
const existingPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, {
|
||||||
page: 1, size: 100,
|
page: 1, size: 100,
|
||||||
dataFilter: { enabled: true, filters: [
|
dataFilter: { enabled: true, filters: [
|
||||||
{ columnName: "customer_id", operator: "equals", value: custKey },
|
{ columnName: "customer_id", operator: "equals", value: custKey },
|
||||||
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
||||||
]}, autoFilter: true,
|
]}, autoFilter: true,
|
||||||
});
|
});
|
||||||
const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || [])
|
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||||
.filter((m: any) => !keptIds.has(m.id));
|
|
||||||
if (toDelete.length > 0) {
|
|
||||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
|
||||||
data: toDelete.map((m: any) => ({ id: m.id })),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
|
|
||||||
if (!firstMappingId) firstMappingId = editCustData.id;
|
// 단가 upsert: 인덱스 기반
|
||||||
|
|
||||||
// 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
|
||||||
const keptPriceIds = new Set<string>();
|
|
||||||
const priceRows = (custPrices[custKey] || []).filter((p) =>
|
const priceRows = (custPrices[custKey] || []).filter((p) =>
|
||||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||||
);
|
);
|
||||||
for (const price of priceRows) {
|
const usedPriceIds = new Set<string>();
|
||||||
if (price._id?.startsWith("p_existing_")) {
|
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||||
const realId = price._id.replace("p_existing_", "");
|
const price = priceRows[pi];
|
||||||
keptPriceIds.add(realId);
|
const priceData = {
|
||||||
|
mapping_id: firstMappingId || editCustData.id,
|
||||||
|
customer_id: custKey, item_id: selectedItem.item_number,
|
||||||
|
start_date: price.start_date || null, end_date: price.end_date || null,
|
||||||
|
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
||||||
|
base_price: price.base_price ? Number(price.base_price) : null,
|
||||||
|
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
|
||||||
|
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
||||||
|
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
||||||
|
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
||||||
|
};
|
||||||
|
const existPrice = existingPriceRows[pi];
|
||||||
|
if (existPrice) {
|
||||||
await apiClient.put(`/table-management/tables/customer_item_prices/edit`, {
|
await apiClient.put(`/table-management/tables/customer_item_prices/edit`, {
|
||||||
originalData: { id: realId },
|
originalData: { id: existPrice.id },
|
||||||
updatedData: {
|
updatedData: priceData,
|
||||||
mapping_id: firstMappingId || "",
|
|
||||||
start_date: price.start_date || null, end_date: price.end_date || null,
|
|
||||||
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
|
||||||
base_price: price.base_price ? Number(price.base_price) : null,
|
|
||||||
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
|
||||||
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
|
||||||
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
usedPriceIds.add(existPrice.id);
|
||||||
} else {
|
} else {
|
||||||
await apiClient.post(`/table-management/tables/customer_item_prices/add`, {
|
await apiClient.post(`/table-management/tables/customer_item_prices/add`, {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(), ...priceData,
|
||||||
mapping_id: firstMappingId || "",
|
|
||||||
customer_id: custKey, item_id: selectedItem.item_number,
|
|
||||||
start_date: price.start_date || null, end_date: price.end_date || null,
|
|
||||||
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
|
||||||
base_price: price.base_price ? Number(price.base_price) : null,
|
|
||||||
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
|
||||||
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
|
||||||
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 폼에서 삭제된 단가 → DB 삭제
|
// 초과분 delete
|
||||||
try {
|
const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.has(p.id));
|
||||||
const dbPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, {
|
if (toDeletePrices.length > 0) {
|
||||||
page: 1, size: 100,
|
await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, {
|
||||||
dataFilter: { enabled: true, filters: [
|
data: toDeletePrices.map((p: any) => ({ id: p.id })),
|
||||||
{ columnName: "customer_id", operator: "equals", value: custKey },
|
|
||||||
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
|
||||||
]}, autoFilter: true,
|
|
||||||
});
|
});
|
||||||
const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || [])
|
}
|
||||||
.filter((p: any) => !keptPriceIds.has(p.id));
|
|
||||||
if (toDeletePrices.length > 0) {
|
|
||||||
await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, {
|
|
||||||
data: toDeletePrices.map((p: any) => ({ id: p.id })),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch { /* skip */ }
|
|
||||||
} else {
|
} else {
|
||||||
// 신규 등록
|
// 신규 등록
|
||||||
const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
||||||
@@ -807,6 +811,7 @@ export default function SalesItemPage() {
|
|||||||
start_date: price.start_date || null, end_date: price.end_date || null,
|
start_date: price.start_date || null, end_date: price.end_date || null,
|
||||||
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
|
||||||
base_price: price.base_price ? Number(price.base_price) : null,
|
base_price: price.base_price ? Number(price.base_price) : null,
|
||||||
|
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
|
||||||
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
|
||||||
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
|
||||||
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
|
||||||
@@ -823,7 +828,10 @@ export default function SalesItemPage() {
|
|||||||
setSelectedItemId(null);
|
setSelectedItemId(null);
|
||||||
setTimeout(() => setSelectedItemId(sid), 50);
|
setTimeout(() => setSelectedItemId(sid), 50);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err.response?.data?.message || "저장에 실패했습니다.");
|
console.error("거래처 상세 저장 실패:", err.response?.data);
|
||||||
|
const detail = err.response?.data?.error?.details;
|
||||||
|
const msg = err.response?.data?.message || "저장에 실패했습니다.";
|
||||||
|
toast.error(detail ? `${msg} (${typeof detail === "string" ? detail : JSON.stringify(detail)})` : msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1716,7 +1724,7 @@ export default function SalesItemPage() {
|
|||||||
|
|
||||||
{/* ── 거래처 상세 입력/수정 모달 ── */}
|
{/* ── 거래처 상세 입력/수정 모달 ── */}
|
||||||
<Dialog open={custDetailOpen} onOpenChange={setCustDetailOpen}>
|
<Dialog open={custDetailOpen} onOpenChange={setCustDetailOpen}>
|
||||||
<DialogContent className="max-w-[1100px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-[1100px] h-[85vh] flex flex-col overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
거래처 상세정보 {editCustData ? "수정" : "입력"} — {selectedItem?.item_name || ""}
|
거래처 상세정보 {editCustData ? "수정" : "입력"} — {selectedItem?.item_name || ""}
|
||||||
@@ -1726,7 +1734,7 @@ export default function SalesItemPage() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-6 py-2">
|
<div className="space-y-6 py-2 flex-1 overflow-y-auto">
|
||||||
{selectedCustsForDetail.map((cust, idx) => {
|
{selectedCustsForDetail.map((cust, idx) => {
|
||||||
const custKey = cust.customer_code || cust.id;
|
const custKey = cust.customer_code || cust.id;
|
||||||
const mappingRows = custMappings[custKey] || [];
|
const mappingRows = custMappings[custKey] || [];
|
||||||
@@ -1742,17 +1750,17 @@ export default function SalesItemPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4 p-4 bg-card">
|
<div className="flex gap-4 p-4 bg-card items-stretch">
|
||||||
{/* 좌: 거래처 품번/품명 */}
|
{/* 좌: 거래처 품번/품명 */}
|
||||||
<div className="flex-1 border rounded-lg p-4 bg-muted/50">
|
<div className="flex-1 border rounded-lg p-4 bg-muted/50 flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3 shrink-0">
|
||||||
<span className="text-sm font-semibold text-foreground">거래처 품번/품명 관리</span>
|
<span className="text-sm font-semibold text-foreground">거래처 품번/품명 관리</span>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addMappingRow(custKey)}>
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addMappingRow(custKey)}>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
품번 추가
|
품번 추가
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 min-h-[200px] max-h-[350px] overflow-y-auto flex-1">
|
||||||
{mappingRows.length === 0 ? (
|
{mappingRows.length === 0 ? (
|
||||||
<div className="text-xs text-muted-foreground py-2">입력된 거래처 품번이 없어요</div>
|
<div className="text-xs text-muted-foreground py-2">입력된 거래처 품번이 없어요</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1792,35 +1800,61 @@ export default function SalesItemPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우: 기간별 단가 */}
|
{/* 우: 기간별 단가 */}
|
||||||
<div className="flex-1 border rounded-lg p-4 bg-muted/30">
|
<div className="flex-1 border rounded-lg p-4 bg-muted/30 flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3 shrink-0">
|
||||||
<span className="text-sm font-semibold text-foreground">기간별 단가 설정</span>
|
<span className="text-sm font-semibold text-foreground">기간별 단가 설정</span>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addPriceRow(custKey)}>
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addPriceRow(custKey)}>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
단가 추가
|
단가 추가
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 flex-1 overflow-y-auto max-h-[350px]">
|
||||||
{prices.map((price, pIdx) => (
|
{prices.map((price, pIdx) => (
|
||||||
<div key={price._id} className="border rounded-lg p-3 bg-background space-y-2">
|
<div key={price._id} className="border rounded-lg bg-background overflow-hidden">
|
||||||
<div className="flex items-center justify-between">
|
<div
|
||||||
<span className="text-xs font-medium text-muted-foreground">단가 {pIdx + 1}</span>
|
className="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
|
onClick={() => setCollapsedPriceCards((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(price._id)) next.delete(price._id); else next.add(price._id);
|
||||||
|
return next;
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{collapsedPriceCards.has(price._id)
|
||||||
|
? <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
: <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
}
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">단가 {pIdx + 1}</span>
|
||||||
|
{collapsedPriceCards.has(price._id) && price.calculated_price && (
|
||||||
|
<span className="text-xs text-muted-foreground ml-2">
|
||||||
|
{price.start_date || "—"} ~ {price.end_date || "—"} · <span className="font-mono font-bold text-foreground">{Number(price.calculated_price).toLocaleString()}</span> {priceCategoryOptions["currency_code"]?.find((o) => o.code === price.currency_code)?.label || ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{prices.length > 1 && (
|
{prices.length > 1 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost" size="sm"
|
variant="ghost" size="sm"
|
||||||
className="h-6 w-6 p-0 text-destructive"
|
className="h-6 w-6 p-0 text-destructive"
|
||||||
onClick={() => removePriceRow(custKey, price._id)}
|
onClick={(e) => { e.stopPropagation(); removePriceRow(custKey, price._id); }}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{!collapsedPriceCards.has(price._id) && <div className="px-3 pb-3 space-y-2">
|
||||||
{/* 기간 + 통화 */}
|
{/* 기간 + 통화 */}
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
value={price.start_date}
|
value={price.start_date}
|
||||||
onChange={(e) => updatePriceRow(custKey, price._id, "start_date", e.target.value)}
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
updatePriceRow(custKey, price._id, "start_date", v);
|
||||||
|
if (price.end_date && v > price.end_date) {
|
||||||
|
updatePriceRow(custKey, price._id, "end_date", v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
max={price.end_date || undefined}
|
||||||
className="h-8 text-xs flex-1"
|
className="h-8 text-xs flex-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">~</span>
|
<span className="text-xs text-muted-foreground">~</span>
|
||||||
@@ -1828,6 +1862,7 @@ export default function SalesItemPage() {
|
|||||||
type="date"
|
type="date"
|
||||||
value={price.end_date}
|
value={price.end_date}
|
||||||
onChange={(e) => updatePriceRow(custKey, price._id, "end_date", e.target.value)}
|
onChange={(e) => updatePriceRow(custKey, price._id, "end_date", e.target.value)}
|
||||||
|
min={price.start_date || undefined}
|
||||||
className="h-8 text-xs flex-1"
|
className="h-8 text-xs flex-1"
|
||||||
/>
|
/>
|
||||||
<div className="w-[80px]">
|
<div className="w-[80px]">
|
||||||
@@ -1922,6 +1957,7 @@ export default function SalesItemPage() {
|
|||||||
{price.calculated_price ? Number(price.calculated_price).toLocaleString() : "-"}
|
{price.calculated_price ? Number(price.calculated_price).toLocaleString() : "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -999,7 +999,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테마 토글 */}
|
{/* 테마 토글 */}
|
||||||
{(!isMobile && sidebarCollapsed) ? null : (
|
{(!isMobile && sidebarCollapsed) ? (
|
||||||
|
<div className="flex items-center justify-center border-t border-border py-2">
|
||||||
|
<ThemeToggle collapsed />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="border-border border-t px-3 py-1">
|
<div className="border-border border-t px-3 py-1">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ export function ThemeToggle({ collapsed = false }: ThemeToggleProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={collapsed ? "icon" : "default"}
|
size={collapsed ? "icon" : "default"}
|
||||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||||
className="w-full justify-start gap-2 text-sm"
|
className={collapsed ? "h-10 w-10 justify-center" : "w-full justify-start gap-2 text-sm"}
|
||||||
title={isDark ? "라이트 모드" : "다크 모드"}
|
title={isDark ? "라이트 모드" : "다크 모드"}
|
||||||
>
|
>
|
||||||
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
{isDark ? <Sun className={collapsed ? "h-5 w-5" : "h-4 w-4"} /> : <Moon className={collapsed ? "h-5 w-5" : "h-4 w-4"} />}
|
||||||
{!collapsed && (isDark ? "라이트 모드" : "다크 모드")}
|
{!collapsed && (isDark ? "라이트 모드" : "다크 모드")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user