Adjust lifecycle allocation UI and account-level shared cost breakdown
This commit is contained in:
1 parent
21ad66c8b4
commit
9891ea0a32
2 files changed
+380
-40
No files matched your search
@@ -1925,6 +1925,7 @@
|
||||
const [lifecycleAllocationModal, setLifecycleAllocationModal] = useState(null);
|
||||
const [lifecycleAllocationSaving, setLifecycleAllocationSaving] = useState(false);
|
||||
const [lifecycleCommonAllocationSaving, setLifecycleCommonAllocationSaving] = useState(false);
|
||||
const [lifecycleCommonAllocationDraft, setLifecycleCommonAllocationDraft] = useState("");
|
||||
const [projectEditModalOpen, setProjectEditModalOpen] = useState(false);
|
||||
const [relatedProjectSearch, setRelatedProjectSearch] = useState("");
|
||||
const [projectTxnDateFrom, setProjectTxnDateFrom] = useState("");
|
||||
@@ -1963,6 +1964,13 @@
|
||||
});
|
||||
const deferredProjectKeyword = useDeferredValue(projectKeyword);
|
||||
const deferredVendorKeyword = useDeferredValue(vendorKeyword);
|
||||
const effectiveCommonAllocationMode = lifecycleCommonAllocationDraft
|
||||
|| detail?.lifecycle_cost?.summary?.common_allocation_mode
|
||||
|| "expense_ratio";
|
||||
|
||||
useEffect(() => {
|
||||
setLifecycleCommonAllocationDraft("");
|
||||
}, [selectedProjectCode]);
|
||||
const debouncedProjectKeyword = useDebouncedValue(deferredProjectKeyword, 250);
|
||||
const debouncedVendorKeyword = useDebouncedValue(deferredVendorKeyword, 250);
|
||||
const deferredRelatedProjectSearch = useDeferredValue(relatedProjectSearch);
|
||||
@@ -3609,6 +3617,8 @@
|
||||
async function saveLifecycleCommonAllocationMode(nextMode) {
|
||||
if (!selectedProjectCode) return;
|
||||
if (!["expense_ratio", "income_ratio"].includes(nextMode || "")) return;
|
||||
if (effectiveCommonAllocationMode === nextMode) return;
|
||||
setLifecycleCommonAllocationDraft(nextMode);
|
||||
setLifecycleCommonAllocationSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -3628,6 +3638,7 @@
|
||||
setDetail(nextDetail);
|
||||
}
|
||||
} catch (err) {
|
||||
setLifecycleCommonAllocationDraft("");
|
||||
setError("공통배분 기준 저장에 실패했습니다.");
|
||||
} finally {
|
||||
setLifecycleCommonAllocationSaving(false);
|
||||
@@ -5211,17 +5222,36 @@
|
||||
<div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700 }}>프로젝트 생애주기 원가</div>
|
||||
<div className="subtle" style={{ marginTop: 6 }}>현재 시공 프로젝트를 포함해 연결된 전체 비용을 시공비, 인건비, 관리비로 나눠 봅니다.</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10 }}>
|
||||
<span className="subtle">공통배분 기준</span>
|
||||
<select
|
||||
value={detail?.lifecycle_cost?.summary?.common_allocation_mode || "expense_ratio"}
|
||||
onChange={(e) => saveLifecycleCommonAllocationMode(e.target.value)}
|
||||
disabled={lifecycleCommonAllocationSaving}
|
||||
style={{ minWidth: 220 }}
|
||||
>
|
||||
<option value="expense_ratio">프로젝트 지출 / 전체지출 비율</option>
|
||||
<option value="income_ratio">프로젝트 입금 / 전체입금 비율</option>
|
||||
</select>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 10, minWidth: 0 }}>
|
||||
<span className="subtle" style={{ whiteSpace: "nowrap", flex: "0 0 auto" }}>공통배분 기준</span>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={`mode-chip ${effectiveCommonAllocationMode === "expense_ratio" ? "active" : ""}`}
|
||||
onClick={() => saveLifecycleCommonAllocationMode("expense_ratio")}
|
||||
disabled={lifecycleCommonAllocationSaving}
|
||||
style={{ minWidth: 116, height: 34, fontSize: 13, borderRadius: 10, padding: "0 10px" }}
|
||||
>
|
||||
지출기준
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`mode-chip ${effectiveCommonAllocationMode === "income_ratio" ? "active" : ""}`}
|
||||
onClick={() => saveLifecycleCommonAllocationMode("income_ratio")}
|
||||
disabled={lifecycleCommonAllocationSaving}
|
||||
style={{ minWidth: 116, height: 34, fontSize: 13, borderRadius: 10, padding: "0 10px" }}
|
||||
>
|
||||
수입기준
|
||||
</button>
|
||||
</div>
|
||||
<div className="subtle" style={{ marginLeft: 12, lineHeight: 1.45, minWidth: 0 }}>
|
||||
<div style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
|
||||
</div>
|
||||
<div style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
기준값: {effectiveCommonAllocationMode === "income_ratio" ? "프로젝트 입금 / 전체입금" : "프로젝트 지출 / 전체지출"}
|
||||
</div>
|
||||
</div>
|
||||
<span className="subtle">{lifecycleCommonAllocationSaving ? "저장 중..." : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5393,6 +5423,12 @@
|
||||
bucket_label: lifecycleBreakdownModal.label,
|
||||
account_code: item.account_code || "",
|
||||
account_name: item.account_name || "",
|
||||
allocation_source_amount: Number(item.allocation_source_amount || 0),
|
||||
allocation_project_basis_amount: Number(item.allocation_project_basis_amount || 0),
|
||||
allocation_total_basis_amount: Number(item.allocation_total_basis_amount || 0),
|
||||
allocation_mode: item.allocation_mode || "",
|
||||
allocation_result_amount: Number(item.expense_supply || 0),
|
||||
allocation_details: Array.isArray(item.allocation_details) ? item.allocation_details.map((row) => ({ ...row })) : [],
|
||||
detail: null,
|
||||
});
|
||||
}}
|
||||
@@ -5413,6 +5449,37 @@
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{item.account_name || "미지정 계정"}</div>
|
||||
<div className="subtle" style={{ marginTop: 4 }}>{item.account_code || "-"}</div>
|
||||
{((item.account_code || "").startsWith("SHARED_")) && (
|
||||
<div className="subtle" style={{ marginTop: 4 }}>
|
||||
본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
|
||||
<br />
|
||||
기준값: {item.allocation_mode === "income_ratio" ? "프로젝트 입금 / 전체입금" : "프로젝트 지출 / 전체지출"}
|
||||
<br />
|
||||
{(Array.isArray(item.allocation_details) && item.allocation_details.length
|
||||
? item.allocation_details
|
||||
: [{
|
||||
source_amount: item.allocation_source_amount || 0,
|
||||
project_basis_amount: item.allocation_project_basis_amount || 0,
|
||||
total_basis_amount: item.allocation_total_basis_amount || 0,
|
||||
allocated_amount: item.expense_supply || 0,
|
||||
}]
|
||||
).map((row, idx) => {
|
||||
const sourceAmount = Number(row.source_amount || 0);
|
||||
const allocatedAmount = Number(row.allocated_amount || 0);
|
||||
const projectBasisAmount = Number(
|
||||
row.display_project_basis_amount ?? row.project_basis_amount ?? 0
|
||||
);
|
||||
const totalBasisAmount = Number(
|
||||
row.display_total_basis_amount ?? row.total_basis_amount ?? 0
|
||||
);
|
||||
return (
|
||||
<span key={`alloc-line-${item.account_code}-${idx}`} style={{ display: "block" }}>
|
||||
{((row.year_month || "").slice(0, 4) || "-")}년 · {fmt(sourceAmount)}원 x ({fmt(projectBasisAmount)}원 / {fmt(totalBasisAmount)}원) = {fmt(allocatedAmount)}원
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: "right", fontWeight: 700 }}>{fmt(item.expense_supply || 0)}원</div>
|
||||
</button>
|
||||
@@ -6755,6 +6822,48 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: 18, marginTop: 18 }}>
|
||||
{((lifecycleAccountDetailModal.account_code || "").startsWith("SHARED_") ||
|
||||
Number(lifecycleAccountDetailModal.allocation_source_amount || 0) > 0 ||
|
||||
Number(lifecycleAccountDetailModal.detail?.allocation_source_amount || 0) > 0) && (
|
||||
<section className="mini-card">
|
||||
<div style={{ fontSize: 16, fontWeight: 700 }}>배부 계산식</div>
|
||||
<div className="subtle" style={{ marginTop: 6 }}>
|
||||
본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
|
||||
</div>
|
||||
<div className="subtle" style={{ marginTop: 4 }}>
|
||||
기준값: {(lifecycleAccountDetailModal.detail?.allocation_mode || lifecycleAccountDetailModal.allocation_mode) === "income_ratio"
|
||||
? "프로젝트 입금 / 전체입금"
|
||||
: "프로젝트 지출 / 전체지출"}
|
||||
</div>
|
||||
<div className="subtle" style={{ marginTop: 8, lineHeight: 1.55 }}>
|
||||
{(() => {
|
||||
const rows = (Array.isArray(lifecycleAccountDetailModal.detail?.allocation_details) && lifecycleAccountDetailModal.detail.allocation_details.length)
|
||||
? lifecycleAccountDetailModal.detail.allocation_details
|
||||
: (Array.isArray(lifecycleAccountDetailModal.allocation_details) ? lifecycleAccountDetailModal.allocation_details : []);
|
||||
const fallback = [{
|
||||
year_month: "",
|
||||
source_amount: lifecycleAccountDetailModal.detail?.allocation_source_amount ?? lifecycleAccountDetailModal.allocation_source_amount ?? 0,
|
||||
project_basis_amount: lifecycleAccountDetailModal.detail?.allocation_project_basis_amount ?? lifecycleAccountDetailModal.allocation_project_basis_amount ?? 0,
|
||||
total_basis_amount: lifecycleAccountDetailModal.detail?.allocation_total_basis_amount ?? lifecycleAccountDetailModal.allocation_total_basis_amount ?? 0,
|
||||
allocated_amount: lifecycleAccountDetailModal.allocation_result_amount ?? 0,
|
||||
}];
|
||||
const list = rows.length ? rows : fallback;
|
||||
return list.map((row, idx) => {
|
||||
const sourceAmount = Number(row.source_amount || 0);
|
||||
const allocatedAmount = Number(row.allocated_amount || 0);
|
||||
const projectBasisAmount = Number(row.display_project_basis_amount ?? row.project_basis_amount ?? 0);
|
||||
const totalBasisAmount = Number(row.display_total_basis_amount ?? row.total_basis_amount ?? 0);
|
||||
return (
|
||||
<div key={`alloc-restore-${idx}`}>
|
||||
{((row.year_month || "").slice(0, 4) || "-")}년 · {fmt(sourceAmount)}원 x ({fmt(projectBasisAmount)}원 / {fmt(totalBasisAmount)}원) = {fmt(allocatedAmount)}원
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="mini-card">
|
||||
<div style={{ fontSize: 16, fontWeight: 700 }}>프로젝트별 금액</div>
|
||||
<div className="subtle" style={{ marginTop: 6 }}>연결된 프로젝트들 중 이 계정에 포함된 지출입니다.</div>
|
||||
@@ -6831,6 +6940,38 @@
|
||||
<div className="empty-state">표시할 거래내역이 없습니다.</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mini-card">
|
||||
<div style={{ fontSize: 16, fontWeight: 700 }}>계정별 금액</div>
|
||||
<div className="subtle" style={{ marginTop: 6 }}>현재 선택한 계정 기준 금액입니다.</div>
|
||||
<div style={{ display: "grid", gap: 10, marginTop: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "left",
|
||||
border: "1px solid var(--line)",
|
||||
borderRadius: 14,
|
||||
background: "white",
|
||||
padding: "12px 14px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(220px, 1fr) minmax(140px, 0.4fr)",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{lifecycleAccountDetailModal.account_name || "(계정명없음)"}</div>
|
||||
<div className="subtle" style={{ marginTop: 4 }}>{lifecycleAccountDetailModal.account_code || "-"}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: "right", fontWeight: 700 }}>
|
||||
{fmt(
|
||||
(lifecycleAccountDetailModal.account_code || "").startsWith("SHARED_")
|
||||
? Number(lifecycleAccountDetailModal.allocation_result_amount || 0)
|
||||
: Number(lifecycleAccountDetailModal.detail?.summary?.expense_supply_sum || 0)
|
||||
)}원
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user