[as] feat: 행정지원 숫자 폼 소숫점 자리 제한
This commit is contained in:
@@ -27,6 +27,7 @@ settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
.agent
|
||||
CLAUDE.MD
|
||||
|
||||
apps/**/tsconfig.tsbuildinfo
|
||||
packages/**/tsconfig.tsbuildinfo
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface CnstpSeMngResponseDto {
|
||||
cnstpSeCdNm: string;
|
||||
upCnstpCd?: string;
|
||||
cdLvNo: number;
|
||||
numYn: string;
|
||||
dcptUnitCnt: number;
|
||||
useYn: string;
|
||||
expln?: string;
|
||||
sortSeq?: number;
|
||||
@@ -41,6 +43,8 @@ export interface CnstpSeMngSaveRequestDto {
|
||||
useYn?: string;
|
||||
expln?: string;
|
||||
sortSeq?: number;
|
||||
numYn?: string;
|
||||
dcptUnitCnt?: number;
|
||||
}
|
||||
|
||||
/** PUT /{cnstpSeCd} 요청 (mode는 서비스에서 U로 설정) */
|
||||
@@ -51,6 +55,8 @@ export interface CnstpSeMngPutRequestDto {
|
||||
cdLvNo?: number;
|
||||
useYn?: string;
|
||||
expln?: string;
|
||||
numYn?: string;
|
||||
dcptUnitCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 CnstpSeMngTreeDto */
|
||||
@@ -60,8 +66,11 @@ export interface CnstpSeMngTreeDto {
|
||||
cnstpSeCdNm: string;
|
||||
upCnstpCd?: string;
|
||||
useYn: string;
|
||||
numYn: string;
|
||||
dcptUnitCnt?: number;
|
||||
treeLevel: number;
|
||||
treePath: string;
|
||||
sortPath: string;
|
||||
}
|
||||
|
||||
/** 백엔드 BizSclTreeDto */
|
||||
@@ -70,8 +79,11 @@ export interface BizSclTreeDto {
|
||||
cnstpSeCd: string;
|
||||
cnstpSeCdNm: string;
|
||||
useYn: string;
|
||||
numYn: string;
|
||||
dcptUnitCnt?: number;
|
||||
treeLevel: number;
|
||||
treePath: string;
|
||||
sortPath: string;
|
||||
bscPlanQty?: number;
|
||||
bgtRfltQty?: number;
|
||||
tstmDesignQty?: number;
|
||||
|
||||
@@ -21,6 +21,8 @@ const buildSavePayload = createSavePayloadBuilder<CnstpSeRow, CnstpSeMngSaveRequ
|
||||
useYn: row.useYn,
|
||||
expln: row.expln?.trim() || undefined,
|
||||
sortSeq: row.sortSeq,
|
||||
numYn: row.numYn,
|
||||
dcptUnitCnt: row.dcptUnitCnt,
|
||||
}),
|
||||
toCreateDto: (row) => ({
|
||||
mode: CNSTP_SE_SAVE_MODE.CREATE,
|
||||
@@ -31,6 +33,8 @@ const buildSavePayload = createSavePayloadBuilder<CnstpSeRow, CnstpSeMngSaveRequ
|
||||
useYn: row.useYn,
|
||||
expln: row.expln?.trim() || undefined,
|
||||
sortSeq: row.sortSeq,
|
||||
numYn: row.numYn,
|
||||
dcptUnitCnt: row.dcptUnitCnt,
|
||||
}),
|
||||
toUpdateDto: (row) => ({
|
||||
mode: CNSTP_SE_SAVE_MODE.UPDATE,
|
||||
@@ -41,6 +45,8 @@ const buildSavePayload = createSavePayloadBuilder<CnstpSeRow, CnstpSeMngSaveRequ
|
||||
useYn: row.useYn,
|
||||
expln: row.expln?.trim() || undefined,
|
||||
sortSeq: row.sortSeq,
|
||||
numYn: row.numYn,
|
||||
dcptUnitCnt: row.dcptUnitCnt,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -62,6 +68,8 @@ export const cnstpGridConfig: SeGridConfig<
|
||||
cnstpSeCd: "",
|
||||
cnstpSeCdNm: "",
|
||||
expln: "",
|
||||
numYn: "Y",
|
||||
dcptUnitCnt: 0,
|
||||
useYn: "Y",
|
||||
}),
|
||||
applyDraftPatch: (draft, patch) => ({
|
||||
@@ -69,6 +77,8 @@ export const cnstpGridConfig: SeGridConfig<
|
||||
...(patch.cnstpSeCd !== undefined ? { cnstpSeCd: patch.cnstpSeCd } : {}),
|
||||
...(patch.cnstpSeCdNm !== undefined ? { cnstpSeCdNm: patch.cnstpSeCdNm ?? "" } : {}),
|
||||
...(patch.expln !== undefined ? { expln: patch.expln ?? "" } : {}),
|
||||
...(patch.numYn !== undefined ? { numYn: patch.numYn } : {}),
|
||||
...(patch.dcptUnitCnt !== undefined ? { dcptUnitCnt: patch.dcptUnitCnt } : {}),
|
||||
...(patch.useYn !== undefined ? { useYn: patch.useYn } : {}),
|
||||
...(patch.sortSeq !== undefined ? { sortSeq: patch.sortSeq } : {}),
|
||||
}),
|
||||
@@ -77,6 +87,8 @@ export const cnstpGridConfig: SeGridConfig<
|
||||
cnstpSeCd: draft.cnstpSeCd,
|
||||
cnstpSeCdNm: draft.cnstpSeCdNm,
|
||||
expln: draft.expln,
|
||||
numYn: draft.numYn,
|
||||
dcptUnitCnt: draft.dcptUnitCnt,
|
||||
useYn: draft.useYn,
|
||||
cdLvNo: draft.cdLvNo,
|
||||
upCnstpCd: parentCode,
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface CnstpSeDraft {
|
||||
cnstpSeCd: string;
|
||||
cnstpSeCdNm: string;
|
||||
expln: string;
|
||||
numYn: string;
|
||||
dcptUnitCnt: number;
|
||||
useYn: string;
|
||||
sortSeq?: number;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ function CnstpCodeTabPanel() {
|
||||
<TableHead>코드</TableHead>
|
||||
<TableHead>코드명</TableHead>
|
||||
<TableHead>설명</TableHead>
|
||||
<TableHead>숫자 여부</TableHead>
|
||||
<TableHead>소숫점 단위</TableHead>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableHead>구분</TableHead>
|
||||
|
||||
@@ -62,6 +62,33 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
||||
maxLength={1000}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<RadioGroup
|
||||
value={row.numYn === "N" ? "N" : "Y"}
|
||||
onValueChange={(v) => onChange({ numYn: v })}
|
||||
className="flex justify-center gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="Y" id={`${row.rowKey}-num-y`} />
|
||||
<Label htmlFor={`${row.rowKey}-use-y`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="N" id={`${row.rowKey}-num-n`} />
|
||||
<Label htmlFor={`${row.rowKey}-use-n`}>아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={row.dcptUnitCnt ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ dcptUnitCnt: e.target.value ? Number(e.target.value) : undefined })
|
||||
}
|
||||
className="w-20"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<RadioGroup
|
||||
value={row.useYn === "N" ? "N" : "Y"}
|
||||
@@ -89,7 +116,9 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
|
||||
className="w-20"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">{levelLabel}</TableCell>
|
||||
<TableCell align="center" className="whitespace-nowrap">
|
||||
{levelLabel}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { buildAcntgCostItems } from "@/widgets/pbadms-sprt";
|
||||
import { buildAcntgCostItems, truncateDecimals } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Input,
|
||||
Skeleton,
|
||||
@@ -50,7 +50,8 @@ function BizCstMatrixFormSection() {
|
||||
];
|
||||
|
||||
function handleChange(acntgSeCd: string, planStpNo: number, value: string) {
|
||||
const numValue = Number(value) || 0;
|
||||
const normalized = truncateDecimals(value, 3);
|
||||
const numValue = Number(normalized.replace(/,/g, "")) || 0;
|
||||
const next = recalculateBizCstMatrix(
|
||||
setAmt(getValues("bizCstList"), acntgSeCd, planStpNo, numValue),
|
||||
costItems,
|
||||
@@ -113,6 +114,7 @@ function BizCstMatrixFormSection() {
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
step={0.001}
|
||||
value={getAmt(bizCstList, item.code, step.planStpNo) || ""}
|
||||
onChange={(e) => handleChange(item.code, step.planStpNo, e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dynamic from "next/dynamic";
|
||||
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { truncateDecimals } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -28,9 +29,9 @@ const PreviousPlanLookupDialog = dynamic(
|
||||
);
|
||||
|
||||
const OTLN_ROWS = [
|
||||
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구" },
|
||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구" },
|
||||
{ key: "sewerPsplyRt" as const, label: "하수도보급율" },
|
||||
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 },
|
||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 },
|
||||
{ key: "sewerPsplyRt" as const, label: "하수도보급율", step: 0.1, decimals: 1 },
|
||||
];
|
||||
|
||||
function OutlineFormSection() {
|
||||
@@ -58,8 +59,10 @@ function OutlineFormSection() {
|
||||
planStpNo: number,
|
||||
fieldKey: (typeof OTLN_ROWS)[number]["key"],
|
||||
value: string,
|
||||
decimals?: number,
|
||||
) {
|
||||
const numValue = Number(value.replace(/,/g, "")) || 0;
|
||||
const normalized = decimals === undefined ? value : truncateDecimals(value, decimals);
|
||||
const numValue = Number(normalized.replace(/,/g, "")) || 0;
|
||||
const draft = [...getValues("sewerBscPlanOtlnList")];
|
||||
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
|
||||
if (index < 0) return;
|
||||
@@ -104,11 +107,13 @@ function OutlineFormSection() {
|
||||
<TableCell key={planStpNo}>
|
||||
<Field>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
type="number"
|
||||
step={row.step}
|
||||
className="text-right"
|
||||
value={item?.[row.key] ?? ""}
|
||||
onChange={(e) => handleChange(planStpNo, row.key, e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleChange(planStpNo, row.key, e.target.value, row.decimals)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Field } from "@/shared/ui/field";
|
||||
import { attachService } from "@/entities/attach";
|
||||
import { useFacilityList } from "@/entities/bns-if";
|
||||
import { useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { truncateDecimals } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -57,15 +58,17 @@ interface UploadedTmplntFile {
|
||||
const TMPLNT_ATCH_ACCEPT = ".pdf,.png,.jpg,.jpeg";
|
||||
const TMPLNT_ATCH_FORMAT_LABEL = ".pdf, .png, .jpg, .jpeg";
|
||||
|
||||
const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string }[] = [
|
||||
{ key: "prcsZoneArea", label: "처리구역면적(km²)" },
|
||||
const TMPLNT_DECIMAL_PLACES = 3;
|
||||
|
||||
const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string; decimals?: number }[] = [
|
||||
{ key: "prcsZoneArea", label: "처리구역면적(km²)", decimals: TMPLNT_DECIMAL_PLACES },
|
||||
{ key: "swtrmtPpltnCnt", label: "하수처리인구(명)" },
|
||||
{ key: "planSwqty", label: "계획하수량(㎥/일)" },
|
||||
{ key: "fcltCpct", label: "시설용량(㎥/일)" },
|
||||
{ key: "sprsysDrtwPplLen", label: "분류식 오수(km)" },
|
||||
{ key: "sprsysExlnPplLen", label: "분류식 우수(km)" },
|
||||
{ key: "cbsysPplLen", label: "합류식(km)" },
|
||||
{ key: "icptgPplLen", label: "차집관로(km)" },
|
||||
{ key: "sprsysDrtwPplLen", label: "분류식 오수(km)", decimals: TMPLNT_DECIMAL_PLACES },
|
||||
{ key: "sprsysExlnPplLen", label: "분류식 우수(km)", decimals: TMPLNT_DECIMAL_PLACES },
|
||||
{ key: "cbsysPplLen", label: "합류식(km)", decimals: TMPLNT_DECIMAL_PLACES },
|
||||
{ key: "icptgPplLen", label: "차집관로(km)", decimals: TMPLNT_DECIMAL_PLACES },
|
||||
{ key: "drgCnt", label: "배수설비(개소)" },
|
||||
{ key: "drtwRlyPumpCnt", label: "오수 중계 펌프(개소)" },
|
||||
{ key: "drtwPumpCnt", label: "오수 일반 펌프(개소)" },
|
||||
@@ -76,12 +79,14 @@ interface TmplntDtlInputProps {
|
||||
facilityIndex: number;
|
||||
planStpNo: number;
|
||||
fieldKey: TmplntFieldKey;
|
||||
decimals?: number;
|
||||
}
|
||||
|
||||
function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey }: TmplntDtlInputProps) {
|
||||
function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey, decimals }: TmplntDtlInputProps) {
|
||||
const { control } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const fieldName =
|
||||
`sewerBscPlanTmplntList.${facilityIndex}.sewerBscPlanTmplntDtlList.${planStpNo}.${fieldKey}` as const;
|
||||
const hasDecimals = decimals !== undefined;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
@@ -90,13 +95,15 @@ function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey }: TmplntDtlInputPr
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<Input
|
||||
type="text"
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step={hasDecimals ? Number(`1e-${decimals}`) : undefined}
|
||||
className="text-right"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const numValue = Number(e.target.value.replace(/,/g, "")) || 0;
|
||||
field.onChange(numValue);
|
||||
const value = e.target.value.replace(/,/g, "");
|
||||
const normalized = hasDecimals ? truncateDecimals(value, decimals) : value;
|
||||
field.onChange(Number(normalized) || 0);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
@@ -193,7 +200,12 @@ function TmplntFacilityMatrix({
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell key={`current-${row.key}`}>
|
||||
<TmplntDtlInput facilityIndex={facilityIndex} planStpNo={0} fieldKey={row.key} />
|
||||
<TmplntDtlInput
|
||||
facilityIndex={facilityIndex}
|
||||
planStpNo={0}
|
||||
fieldKey={row.key}
|
||||
decimals={row.decimals}
|
||||
/>
|
||||
</TableCell>
|
||||
{changeStepHeaders.map((step) => (
|
||||
<TableCell key={`change-${row.key}-${step.planStpNo}`}>
|
||||
@@ -201,6 +213,7 @@ function TmplntFacilityMatrix({
|
||||
facilityIndex={facilityIndex}
|
||||
planStpNo={step.planStpNo}
|
||||
fieldKey={row.key}
|
||||
decimals={row.decimals}
|
||||
/>
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
@@ -30,7 +30,13 @@ export {
|
||||
toFnrsSharSaveList,
|
||||
validateBizCstSharBalance,
|
||||
} from "./lib/fnrs-shar-form-utils";
|
||||
export { calcDiffPercent, fmtNumber, indentPx, roundAmount } from "./lib/format-utils";
|
||||
export {
|
||||
calcDiffPercent,
|
||||
fmtNumber,
|
||||
indentPx,
|
||||
roundAmount,
|
||||
truncateDecimals,
|
||||
} from "./lib/format-utils";
|
||||
export { AttachmentsFormSection } from "./ui/attachments-form-section";
|
||||
export { AttachmentsViewSection } from "./ui/attachments-view-section";
|
||||
export { BusinessCostFormSection } from "./ui/business-cost-form-section";
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { AcntgSeMngTreeDto } from "@/entities/acntg";
|
||||
|
||||
import { roundAmount } from "./format-utils";
|
||||
|
||||
/** 세부별 총사업비 소수점 자릿수 (단위: 백만원) */
|
||||
export const COST_DECIMAL_PLACES = 3;
|
||||
|
||||
export interface CostItemDef {
|
||||
code: string;
|
||||
label: string;
|
||||
@@ -32,9 +35,9 @@ function sumChildAmounts(list: BizCstListItem[], childCodes: string[]) {
|
||||
});
|
||||
|
||||
return {
|
||||
bscPlanAmt: roundAmount(bscPlanAmt),
|
||||
bgtRfltAmt: roundAmount(bgtRfltAmt),
|
||||
tstmDesignAmt: roundAmount(tstmDesignAmt),
|
||||
bscPlanAmt: roundAmount(bscPlanAmt, COST_DECIMAL_PLACES),
|
||||
bgtRfltAmt: roundAmount(bgtRfltAmt, COST_DECIMAL_PLACES),
|
||||
tstmDesignAmt: roundAmount(tstmDesignAmt, COST_DECIMAL_PLACES),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,17 @@ export interface ScaleItemDef {
|
||||
code: string;
|
||||
label: string;
|
||||
level: number;
|
||||
numYn: boolean;
|
||||
step: number;
|
||||
dcptUnitCnt: number;
|
||||
readonly?: boolean;
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
function toStep(dcptUnitCnt?: number) {
|
||||
return dcptUnitCnt && dcptUnitCnt > 0 ? Number(`1e-${dcptUnitCnt}`) : 1;
|
||||
}
|
||||
|
||||
export interface BzScaleListItem {
|
||||
cnstpSeCd: string;
|
||||
bscPl: number;
|
||||
@@ -65,6 +72,9 @@ export function buildCnstpScaleItems(tree: CnstpSeMngTreeDto[]): ScaleItemDef[]
|
||||
code: node.cnstpSeCd,
|
||||
label: node.cnstpSeCdNm,
|
||||
level: node.treeLevel,
|
||||
numYn: node.numYn !== "N",
|
||||
step: toStep(node.dcptUnitCnt),
|
||||
dcptUnitCnt: node.dcptUnitCnt ?? 0,
|
||||
readonly: Boolean(children && children.length > 0),
|
||||
children,
|
||||
};
|
||||
|
||||
@@ -20,11 +20,19 @@ export function indentPx(indNm: string | undefined): number {
|
||||
|
||||
const AMOUNT_DECIMAL_PLACES = 2;
|
||||
|
||||
export function roundAmount(value: number): number {
|
||||
const factor = 10 ** AMOUNT_DECIMAL_PLACES;
|
||||
export function roundAmount(value: number, decimals: number = AMOUNT_DECIMAL_PLACES): number {
|
||||
const factor = 10 ** decimals;
|
||||
return Math.round((value + Number.EPSILON) * factor) / factor;
|
||||
}
|
||||
|
||||
/** 입력 문자열의 소수부를 decimals 자리까지 잘라낸다 */
|
||||
export function truncateDecimals(value: string, decimals: number): string {
|
||||
const dotIndex = value.indexOf(".");
|
||||
if (dotIndex < 0) return value;
|
||||
if (decimals <= 0) return value.slice(0, dotIndex);
|
||||
return value.slice(0, dotIndex + 1 + decimals);
|
||||
}
|
||||
|
||||
export function calcDiffPercent(diff: number, baseline: number): string {
|
||||
if (baseline === 0) return "-";
|
||||
return `${Math.round((diff / baseline) * 100)}%`;
|
||||
|
||||
@@ -17,8 +17,12 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { buildAcntgCostItems, recalculateBizCstTotals } from "../lib/business-cost-form-utils";
|
||||
import { calcDiffPercent } from "../lib/format-utils";
|
||||
import {
|
||||
buildAcntgCostItems,
|
||||
COST_DECIMAL_PLACES,
|
||||
recalculateBizCstTotals,
|
||||
} from "../lib/business-cost-form-utils";
|
||||
import { calcDiffPercent, roundAmount, truncateDecimals } from "../lib/format-utils";
|
||||
|
||||
interface BusinessCostFormData {
|
||||
bizCstList: Array<{
|
||||
@@ -31,6 +35,10 @@ interface BusinessCostFormData {
|
||||
|
||||
type CostFieldKey = "bscPlanAmt" | "bgtRfltAmt" | "tstmDesignAmt";
|
||||
|
||||
/** 컬럼 순서: 예산반영(A), 표준사업비(B), 금회설계시(C) */
|
||||
const COST_FIELDS: CostFieldKey[] = ["bgtRfltAmt", "bscPlanAmt", "tstmDesignAmt"];
|
||||
const COST_STEP = 0.001;
|
||||
|
||||
function isSameBizCstCodes(
|
||||
current: BusinessCostFormData["bizCstList"],
|
||||
next: BusinessCostFormData["bizCstList"],
|
||||
@@ -71,7 +79,7 @@ function BusinessCostFormSection() {
|
||||
}, [costItems, getValues, setValue]);
|
||||
|
||||
function handleCostChange(index: number, fieldKey: CostFieldKey, value: string) {
|
||||
const numValue = Number(value) || 0;
|
||||
const numValue = Number(truncateDecimals(value, COST_DECIMAL_PLACES)) || 0;
|
||||
const draft = [...getValues("bizCstList")];
|
||||
draft[index] = { ...draft[index], [fieldKey]: numValue };
|
||||
const next = recalculateBizCstTotals(draft, costItems);
|
||||
@@ -114,7 +122,10 @@ function BusinessCostFormSection() {
|
||||
const data = bizCstList[idx];
|
||||
if (!data) return null;
|
||||
|
||||
const diff = (data.tstmDesignAmt || 0) - (data.bgtRfltAmt || 0);
|
||||
const diff = roundAmount(
|
||||
(data.tstmDesignAmt || 0) - (data.bgtRfltAmt || 0),
|
||||
COST_DECIMAL_PLACES,
|
||||
);
|
||||
const paddingLeft = `${item.level * 1.6}rem`;
|
||||
const showSubIcon = item.level >= 2;
|
||||
|
||||
@@ -128,39 +139,20 @@ function BusinessCostFormSection() {
|
||||
{item.label}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={data.bgtRfltAmt ?? ""}
|
||||
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={data.bscPlanAmt ?? ""}
|
||||
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={data.tstmDesignAmt ?? ""}
|
||||
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
{COST_FIELDS.map((fieldKey) => (
|
||||
<TableCell key={fieldKey}>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
step={COST_STEP}
|
||||
value={data[fieldKey] ?? ""}
|
||||
onChange={(e) => handleCostChange(idx, fieldKey, e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell>
|
||||
<div className="text-right">
|
||||
{diff > 0 ? `+${diff.toLocaleString()}` : diff.toLocaleString()}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@common/components";
|
||||
|
||||
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
|
||||
import { truncateDecimals } from "../lib/format-utils";
|
||||
|
||||
interface BusinessScaleFormData {
|
||||
bzScaleList: Array<{
|
||||
@@ -31,6 +32,8 @@ interface BusinessScaleFormData {
|
||||
|
||||
type ScaleFieldKey = "bscPl" | "budgetReflct" | "thtiDsgn";
|
||||
|
||||
const SCALE_FIELDS: ScaleFieldKey[] = ["bscPl", "budgetReflct", "thtiDsgn"];
|
||||
|
||||
function isSameBzScaleCodes(
|
||||
current: BusinessScaleFormData["bzScaleList"],
|
||||
next: BusinessScaleFormData["bzScaleList"],
|
||||
@@ -73,7 +76,9 @@ function BusinessScaleFormSection() {
|
||||
}, [scaleItems, getValues, setValue]);
|
||||
|
||||
function handleScaleChange(index: number, fieldKey: ScaleFieldKey, value: string) {
|
||||
const numValue = Number(value) || 0;
|
||||
const item = scaleItems[index];
|
||||
const normalized = item?.numYn ? truncateDecimals(value, item.dcptUnitCnt) : value;
|
||||
const numValue = Number(normalized) || 0;
|
||||
const draft = [...getValues("bzScaleList")];
|
||||
draft[index] = { ...draft[index], [fieldKey]: numValue };
|
||||
const next = recalculateBzScaleTotals(draft, scaleItems);
|
||||
@@ -123,39 +128,20 @@ function BusinessScaleFormSection() {
|
||||
{item.label}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={data.bscPl ?? ""}
|
||||
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={data.budgetReflct ?? ""}
|
||||
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={data.thtiDsgn ?? ""}
|
||||
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
{SCALE_FIELDS.map((fieldKey) => (
|
||||
<TableCell key={fieldKey}>
|
||||
<Field>
|
||||
<Input
|
||||
type={item.numYn ? "number" : "text"}
|
||||
step={item.numYn ? item.step : undefined}
|
||||
value={data[fieldKey] ?? ""}
|
||||
onChange={(e) => handleScaleChange(idx, fieldKey, e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell>
|
||||
<div className="text-right">
|
||||
{diff > 0 ? `+${diff.toLocaleString()}` : diff.toLocaleString()}
|
||||
|
||||
Reference in New Issue
Block a user