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,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
||||
autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
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: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -887,7 +889,8 @@ export default function PurchaseItemPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||
]}, 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) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -928,104 +931,104 @@ export default function PurchaseItemPage() {
|
||||
const mappingRows = suppMappings[custKey] || [];
|
||||
|
||||
if (isEditingExisting && editSuppData?.id) {
|
||||
// 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
||||
const keptIds = new Set<string>();
|
||||
let firstMappingId: string | null = null;
|
||||
// 기존 매핑 조회
|
||||
let existingMaps: any[] = [];
|
||||
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++) {
|
||||
const m = mappingRows[mi];
|
||||
if (m._id?.startsWith("m_existing_")) {
|
||||
const realId = m._id.replace("m_existing_", "");
|
||||
keptIds.add(realId);
|
||||
if (mi === 0) firstMappingId = realId;
|
||||
const existMap = existingMaps[mi];
|
||||
if (existMap) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: realId },
|
||||
originalData: { id: existMap.id },
|
||||
updatedData: {
|
||||
supplier_item_code: m.supplier_item_code || "",
|
||||
supplier_item_name: m.supplier_item_name || "",
|
||||
supplier_item_code: mappingRows[mi].supplier_item_code || "",
|
||||
supplier_item_name: mappingRows[mi].supplier_item_name || "",
|
||||
},
|
||||
});
|
||||
usedExistingIds.add(existMap.id);
|
||||
if (mi === 0) firstMappingId = existMap.id;
|
||||
} 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(),
|
||||
supplier_id: custKey, item_id: selectedItem.item_number,
|
||||
supplier_item_code: m.supplier_item_code || "",
|
||||
supplier_item_name: m.supplier_item_name || "",
|
||||
supplier_item_code: mappingRows[mi].supplier_item_code || "",
|
||||
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 {
|
||||
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,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: custKey },
|
||||
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const toDelete = (dbMappings.data?.data?.data || dbMappings.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 })),
|
||||
});
|
||||
}
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
|
||||
if (!firstMappingId) firstMappingId = editSuppData.id;
|
||||
|
||||
// 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
||||
const keptPriceIds = new Set<string>();
|
||||
// 단가 upsert: 인덱스 기반
|
||||
const priceRows = (suppPrices[custKey] || []).filter((p) =>
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
if (price._id?.startsWith("p_existing_")) {
|
||||
const realId = price._id.replace("p_existing_", "");
|
||||
keptPriceIds.add(realId);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
const price = priceRows[pi];
|
||||
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`, {
|
||||
originalData: { id: realId },
|
||||
updatedData: {
|
||||
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,
|
||||
},
|
||||
originalData: { id: existPrice.id },
|
||||
updatedData: priceData,
|
||||
});
|
||||
usedPriceIds.add(existPrice.id);
|
||||
} else {
|
||||
await apiClient.post(`/table-management/tables/supplier_item_prices/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
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,
|
||||
id: crypto.randomUUID(), ...priceData,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 폼에서 삭제된 단가 → DB 삭제
|
||||
try {
|
||||
const dbPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/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,
|
||||
// 초과분 delete
|
||||
const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.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 })),
|
||||
});
|
||||
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 {
|
||||
// 신규 등록
|
||||
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,
|
||||
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,
|
||||
@@ -1719,7 +1723,7 @@ export default function PurchaseItemPage() {
|
||||
|
||||
{/* ── 공급업체 상세 입력/수정 모달 ── */}
|
||||
<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>
|
||||
<DialogTitle>
|
||||
공급업체 상세정보 {editSuppData ? "수정" : "입력"} — {selectedItem?.item_name || ""}
|
||||
@@ -1729,7 +1733,7 @@ export default function PurchaseItemPage() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-2">
|
||||
<div className="space-y-6 py-2 flex-1 overflow-y-auto">
|
||||
{selectedSuppsForDetail.map((cust, idx) => {
|
||||
const custKey = cust.supplier_code || cust.id;
|
||||
const mappingRows = suppMappings[custKey] || [];
|
||||
|
||||
@@ -264,6 +264,7 @@ export default function SalesItemPage() {
|
||||
calculated_price: string;
|
||||
}>>>({});
|
||||
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,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
||||
autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
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: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -639,7 +642,8 @@ export default function SalesItemPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
|
||||
]}, 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) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -680,104 +684,104 @@ export default function SalesItemPage() {
|
||||
const mappingRows = custMappings[custKey] || [];
|
||||
|
||||
if (isEditingExisting && editCustData?.id) {
|
||||
// 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
||||
const keptIds = new Set<string>();
|
||||
let firstMappingId: string | null = null;
|
||||
// 기존 매핑 조회
|
||||
let existingMaps: any[] = [];
|
||||
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++) {
|
||||
const m = mappingRows[mi];
|
||||
if (m._id?.startsWith("m_existing_")) {
|
||||
const realId = m._id.replace("m_existing_", "");
|
||||
keptIds.add(realId);
|
||||
if (mi === 0) firstMappingId = realId;
|
||||
const existMap = existingMaps[mi];
|
||||
if (existMap) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: realId },
|
||||
originalData: { id: existMap.id },
|
||||
updatedData: {
|
||||
customer_item_code: m.customer_item_code || "",
|
||||
customer_item_name: m.customer_item_name || "",
|
||||
customer_item_code: mappingRows[mi].customer_item_code || "",
|
||||
customer_item_name: mappingRows[mi].customer_item_name || "",
|
||||
},
|
||||
});
|
||||
usedExistingIds.add(existMap.id);
|
||||
if (mi === 0) firstMappingId = existMap.id;
|
||||
} 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(),
|
||||
customer_id: custKey, item_id: selectedItem.item_number,
|
||||
customer_item_code: m.customer_item_code || "",
|
||||
customer_item_name: m.customer_item_name || "",
|
||||
customer_item_code: mappingRows[mi].customer_item_code || "",
|
||||
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 {
|
||||
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,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: custKey },
|
||||
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const toDelete = (dbMappings.data?.data?.data || dbMappings.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 })),
|
||||
});
|
||||
}
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
|
||||
if (!firstMappingId) firstMappingId = editCustData.id;
|
||||
|
||||
// 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
|
||||
const keptPriceIds = new Set<string>();
|
||||
// 단가 upsert: 인덱스 기반
|
||||
const priceRows = (custPrices[custKey] || []).filter((p) =>
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
if (price._id?.startsWith("p_existing_")) {
|
||||
const realId = price._id.replace("p_existing_", "");
|
||||
keptPriceIds.add(realId);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
const price = priceRows[pi];
|
||||
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`, {
|
||||
originalData: { id: realId },
|
||||
updatedData: {
|
||||
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,
|
||||
},
|
||||
originalData: { id: existPrice.id },
|
||||
updatedData: priceData,
|
||||
});
|
||||
usedPriceIds.add(existPrice.id);
|
||||
} else {
|
||||
await apiClient.post(`/table-management/tables/customer_item_prices/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
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,
|
||||
id: crypto.randomUUID(), ...priceData,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 폼에서 삭제된 단가 → DB 삭제
|
||||
try {
|
||||
const dbPrices = await apiClient.post(`/table-management/tables/customer_item_prices/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,
|
||||
// 초과분 delete
|
||||
const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.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 })),
|
||||
});
|
||||
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 {
|
||||
// 신규 등록
|
||||
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,
|
||||
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,
|
||||
@@ -823,7 +828,10 @@ export default function SalesItemPage() {
|
||||
setSelectedItemId(null);
|
||||
setTimeout(() => setSelectedItemId(sid), 50);
|
||||
} 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 {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -1716,7 +1724,7 @@ export default function SalesItemPage() {
|
||||
|
||||
{/* ── 거래처 상세 입력/수정 모달 ── */}
|
||||
<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>
|
||||
<DialogTitle>
|
||||
거래처 상세정보 {editCustData ? "수정" : "입력"} — {selectedItem?.item_name || ""}
|
||||
@@ -1726,7 +1734,7 @@ export default function SalesItemPage() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-2">
|
||||
<div className="space-y-6 py-2 flex-1 overflow-y-auto">
|
||||
{selectedCustsForDetail.map((cust, idx) => {
|
||||
const custKey = cust.customer_code || cust.id;
|
||||
const mappingRows = custMappings[custKey] || [];
|
||||
@@ -1742,17 +1750,17 @@ export default function SalesItemPage() {
|
||||
</span>
|
||||
</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 items-center justify-between mb-3">
|
||||
<div className="flex-1 border rounded-lg p-4 bg-muted/50 flex flex-col">
|
||||
<div className="flex items-center justify-between mb-3 shrink-0">
|
||||
<span className="text-sm font-semibold text-foreground">거래처 품번/품명 관리</span>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addMappingRow(custKey)}>
|
||||
<Plus className="h-3 w-3" />
|
||||
품번 추가
|
||||
</Button>
|
||||
</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 ? (
|
||||
<div className="text-xs text-muted-foreground py-2">입력된 거래처 품번이 없어요</div>
|
||||
) : (
|
||||
@@ -1792,35 +1800,61 @@ export default function SalesItemPage() {
|
||||
</div>
|
||||
|
||||
{/* 우: 기간별 단가 */}
|
||||
<div className="flex-1 border rounded-lg p-4 bg-muted/30">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex-1 border rounded-lg p-4 bg-muted/30 flex flex-col">
|
||||
<div className="flex items-center justify-between mb-3 shrink-0">
|
||||
<span className="text-sm font-semibold text-foreground">기간별 단가 설정</span>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addPriceRow(custKey)}>
|
||||
<Plus className="h-3 w-3" />
|
||||
단가 추가
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 flex-1 overflow-y-auto max-h-[350px]">
|
||||
{prices.map((price, pIdx) => (
|
||||
<div key={price._id} className="border rounded-lg p-3 bg-background space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">단가 {pIdx + 1}</span>
|
||||
<div key={price._id} className="border rounded-lg bg-background overflow-hidden">
|
||||
<div
|
||||
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 && (
|
||||
<Button
|
||||
variant="ghost" size="sm"
|
||||
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" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!collapsedPriceCards.has(price._id) && <div className="px-3 pb-3 space-y-2">
|
||||
{/* 기간 + 통화 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
type="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"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">~</span>
|
||||
@@ -1828,6 +1862,7 @@ export default function SalesItemPage() {
|
||||
type="date"
|
||||
value={price.end_date}
|
||||
onChange={(e) => updatePriceRow(custKey, price._id, "end_date", e.target.value)}
|
||||
min={price.start_date || undefined}
|
||||
className="h-8 text-xs flex-1"
|
||||
/>
|
||||
<div className="w-[80px]">
|
||||
@@ -1922,6 +1957,7 @@ export default function SalesItemPage() {
|
||||
{price.calculated_price ? Number(price.calculated_price).toLocaleString() : "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -999,7 +999,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
</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">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
@@ -32,10 +32,10 @@ export function ThemeToggle({ collapsed = false }: ThemeToggleProps) {
|
||||
variant="ghost"
|
||||
size={collapsed ? "icon" : "default"}
|
||||
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 ? "라이트 모드" : "다크 모드"}
|
||||
>
|
||||
{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 ? "라이트 모드" : "다크 모드")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user