254 lines
7.0 KiB
TypeScript
254 lines
7.0 KiB
TypeScript
import type { FnrsSharItem } from "@/entities/fnrs-cnsltn";
|
|
import type { FnrsSharBizCstTreeDto, FnrsSharSeMngTreeDto } from "@/entities/fnrs-shar-se";
|
|
|
|
import type { BizCstListItem, CostItemDef } from "./business-cost-form-utils";
|
|
import { roundAmount } from "./format-utils";
|
|
|
|
export interface FnrsSharItemDef {
|
|
code: string;
|
|
label: string;
|
|
level: number;
|
|
readonly?: boolean;
|
|
syncFromBizCst?: boolean;
|
|
children?: string[];
|
|
}
|
|
|
|
export interface FnrsSharListItem {
|
|
fnrsSharSeCd: string;
|
|
exstnAmt: number;
|
|
chgRqtAmt: number;
|
|
ajmtAmt?: number | null;
|
|
rmrkCn?: string | null;
|
|
}
|
|
|
|
export type FnrsSharTreeNode = FnrsSharSeMngTreeDto | FnrsSharBizCstTreeDto;
|
|
|
|
function getParentCode(node: FnrsSharTreeNode): string | undefined {
|
|
if ("upFnrsSharSeCd" in node && node.upFnrsSharSeCd) {
|
|
return node.upFnrsSharSeCd;
|
|
}
|
|
|
|
const parts = node.treePath.split("|");
|
|
if (parts.length <= 1) return undefined;
|
|
return parts[parts.length - 2];
|
|
}
|
|
|
|
function sortTreeNodes<T extends { sortPath: string }>(nodes: T[]): T[] {
|
|
return [...nodes].sort((a, b) => a.sortPath.localeCompare(b.sortPath));
|
|
}
|
|
|
|
export function buildFnrsSharItems(tree: FnrsSharTreeNode[]): FnrsSharItemDef[] {
|
|
if (tree.length === 0) return [];
|
|
|
|
const activeNodes = sortTreeNodes(tree.filter((node) => node.useYn === "Y"));
|
|
const childrenMap = new Map<string, string[]>();
|
|
|
|
activeNodes.forEach((node) => {
|
|
const parentCode = getParentCode(node);
|
|
if (!parentCode) return;
|
|
|
|
const siblings = childrenMap.get(parentCode) ?? [];
|
|
siblings.push(node.fnrsSharSeCd);
|
|
childrenMap.set(parentCode, siblings);
|
|
});
|
|
|
|
const levelOneNodes = activeNodes.filter((node) => node.treeLevel === 1);
|
|
const syncFromBizCstCode = sortTreeNodes(levelOneNodes)[0]?.fnrsSharSeCd;
|
|
|
|
return activeNodes.map((node) => {
|
|
const children = childrenMap.get(node.fnrsSharSeCd);
|
|
return {
|
|
code: node.fnrsSharSeCd,
|
|
label: node.fnrsSharSeCdNm,
|
|
level: node.treeLevel,
|
|
readonly: Boolean(children && children.length > 0),
|
|
syncFromBizCst: node.fnrsSharSeCd === syncFromBizCstCode,
|
|
children,
|
|
};
|
|
});
|
|
}
|
|
|
|
function sumChildSharAmounts(list: FnrsSharListItem[], childCodes: string[]) {
|
|
let exstnAmt = 0;
|
|
let chgRqtAmt = 0;
|
|
|
|
childCodes.forEach((childCode) => {
|
|
const child = list.find((item) => item.fnrsSharSeCd === childCode);
|
|
if (!child) return;
|
|
exstnAmt += child.exstnAmt || 0;
|
|
chgRqtAmt += child.chgRqtAmt || 0;
|
|
});
|
|
|
|
return {
|
|
exstnAmt: roundAmount(exstnAmt),
|
|
chgRqtAmt: roundAmount(chgRqtAmt),
|
|
};
|
|
}
|
|
|
|
export function recalculateFnrsSharTotals(
|
|
list: FnrsSharListItem[],
|
|
items: FnrsSharItemDef[],
|
|
): FnrsSharListItem[] {
|
|
const newList = list.map((item) => ({ ...item }));
|
|
let hasChanges = false;
|
|
|
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
const item = items[i];
|
|
if (!item.readonly || !item.children) continue;
|
|
|
|
const idx = newList.findIndex((row) => row.fnrsSharSeCd === item.code);
|
|
if (idx < 0) continue;
|
|
|
|
const { exstnAmt, chgRqtAmt } = sumChildSharAmounts(newList, item.children);
|
|
const current = newList[idx];
|
|
|
|
if ((current.exstnAmt || 0) !== exstnAmt || (current.chgRqtAmt || 0) !== chgRqtAmt) {
|
|
newList[idx] = {
|
|
...current,
|
|
exstnAmt,
|
|
chgRqtAmt,
|
|
ajmtAmt: roundAmount(exstnAmt - chgRqtAmt),
|
|
};
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
|
|
return hasChanges ? newList : list;
|
|
}
|
|
|
|
function findBizCstSyncItem(costItems: CostItemDef[]): CostItemDef | undefined {
|
|
const levelOneReadonly = costItems.filter((item) => item.level === 1 && item.readonly);
|
|
return levelOneReadonly[0];
|
|
}
|
|
|
|
export interface BizCstTotalAmounts {
|
|
bgtRfltAmt: number;
|
|
tstmDesignAmt: number;
|
|
}
|
|
|
|
export interface BizCstSharRemaining {
|
|
exstnRemaining: number;
|
|
chgRqtRemaining: number;
|
|
}
|
|
|
|
export function getBizCstTotalRow(
|
|
bizCstList: BizCstListItem[],
|
|
costItems: CostItemDef[],
|
|
): BizCstTotalAmounts {
|
|
const syncCostItem = findBizCstSyncItem(costItems);
|
|
if (!syncCostItem) return { bgtRfltAmt: 0, tstmDesignAmt: 0 };
|
|
|
|
const costRow = bizCstList.find((item) => item.acntgSeCd === syncCostItem.code);
|
|
if (!costRow) return { bgtRfltAmt: 0, tstmDesignAmt: 0 };
|
|
|
|
return {
|
|
bgtRfltAmt: costRow.bgtRfltAmt || 0,
|
|
tstmDesignAmt: costRow.tstmDesignAmt || 0,
|
|
};
|
|
}
|
|
|
|
export function sumFnrsSharLeafAmounts(
|
|
sharList: FnrsSharListItem[],
|
|
sharItems: FnrsSharItemDef[],
|
|
): { exstnAmt: number; chgRqtAmt: number } {
|
|
const leafItems = sharItems.filter((item) => !item.readonly);
|
|
let exstnAmt = 0;
|
|
let chgRqtAmt = 0;
|
|
|
|
leafItems.forEach((item) => {
|
|
const row = sharList.find((entry) => entry.fnrsSharSeCd === item.code);
|
|
if (!row) return;
|
|
exstnAmt += row.exstnAmt || 0;
|
|
chgRqtAmt += row.chgRqtAmt || 0;
|
|
});
|
|
|
|
return {
|
|
exstnAmt: roundAmount(exstnAmt),
|
|
chgRqtAmt: roundAmount(chgRqtAmt),
|
|
};
|
|
}
|
|
|
|
export function calcBizCstSharRemaining(
|
|
bizCstList: BizCstListItem[],
|
|
sharList: FnrsSharListItem[],
|
|
sharItems: FnrsSharItemDef[],
|
|
costItems: CostItemDef[],
|
|
): BizCstSharRemaining {
|
|
const bizCstTotal = getBizCstTotalRow(bizCstList, costItems);
|
|
const leafSum = sumFnrsSharLeafAmounts(sharList, sharItems);
|
|
|
|
return {
|
|
exstnRemaining: roundAmount(bizCstTotal.bgtRfltAmt - leafSum.exstnAmt),
|
|
chgRqtRemaining: roundAmount(bizCstTotal.tstmDesignAmt - leafSum.chgRqtAmt),
|
|
};
|
|
}
|
|
|
|
export function validateBizCstSharBalance(
|
|
bizCstList: BizCstListItem[],
|
|
sharList: FnrsSharListItem[],
|
|
sharItems: FnrsSharItemDef[],
|
|
costItems: CostItemDef[],
|
|
): string | null {
|
|
const { exstnRemaining, chgRqtRemaining } = calcBizCstSharRemaining(
|
|
bizCstList,
|
|
sharList,
|
|
sharItems,
|
|
costItems,
|
|
);
|
|
|
|
if (exstnRemaining !== 0 || chgRqtRemaining !== 0) {
|
|
return "재원분담 총사업비를 확인해주세요.";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function toFnrsSharSaveList(list: FnrsSharListItem[]): FnrsSharItem[] {
|
|
return list.map((row) => ({
|
|
fnrsSharSeCd: row.fnrsSharSeCd,
|
|
exstnAmt: row.exstnAmt || 0,
|
|
chgRqtAmt: row.chgRqtAmt || 0,
|
|
ajmtAmt: roundAmount((row.exstnAmt || 0) - (row.chgRqtAmt || 0)),
|
|
rmrkCn: row.rmrkCn,
|
|
}));
|
|
}
|
|
|
|
export function mergeFnrsSharTreeWithForm(
|
|
tree: FnrsSharTreeNode[],
|
|
existing: FnrsSharListItem[],
|
|
): FnrsSharListItem[] {
|
|
const items = buildFnrsSharItems(tree);
|
|
const existingMap = new Map(existing.map((row) => [row.fnrsSharSeCd, row]));
|
|
|
|
return items.map((item) => {
|
|
const saved = existingMap.get(item.code);
|
|
if (saved) return saved;
|
|
|
|
return {
|
|
fnrsSharSeCd: item.code,
|
|
exstnAmt: 0,
|
|
chgRqtAmt: 0,
|
|
ajmtAmt: 0,
|
|
rmrkCn: "",
|
|
};
|
|
});
|
|
}
|
|
|
|
export function mapFnrsSharDetToFormList(tree: FnrsSharBizCstTreeDto[]): FnrsSharListItem[] {
|
|
const items = buildFnrsSharItems(tree);
|
|
|
|
return items.map((item) => {
|
|
const node = tree.find((row) => row.fnrsSharSeCd === item.code);
|
|
const exstnAmt = Number(node?.exstnAmt) || 0;
|
|
const chgRqtAmt = Number(node?.chgRqtAmt) || 0;
|
|
|
|
return {
|
|
fnrsSharSeCd: item.code,
|
|
exstnAmt,
|
|
chgRqtAmt,
|
|
ajmtAmt: roundAmount(exstnAmt - chgRqtAmt),
|
|
rmrkCn: node?.rmrkCn ?? "",
|
|
};
|
|
});
|
|
}
|