Refine process cost metrics and project search dropdown behavior
This commit is contained in:
@@ -5288,6 +5288,49 @@ def _safe_ratio(numerator: Any, denominator: Any) -> float:
|
|||||||
return (normalize_amount(numerator) / denom) * 100.0
|
return (normalize_amount(numerator) / denom) * 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def _build_process_cost_related_clusters() -> dict[str, list[str]]:
|
||||||
|
related_map = get_project_related_links_map()
|
||||||
|
adjacency: dict[str, set[str]] = {}
|
||||||
|
for base_code, related_codes in related_map.items():
|
||||||
|
normalized_base = normalize_text(base_code)
|
||||||
|
if not normalized_base:
|
||||||
|
continue
|
||||||
|
adjacency.setdefault(normalized_base, set())
|
||||||
|
for related_code in related_codes:
|
||||||
|
normalized_related = normalize_text(related_code)
|
||||||
|
if not normalized_related:
|
||||||
|
continue
|
||||||
|
adjacency.setdefault(normalized_base, set()).add(normalized_related)
|
||||||
|
adjacency.setdefault(normalized_related, set()).add(normalized_base)
|
||||||
|
|
||||||
|
cluster_map: dict[str, list[str]] = {}
|
||||||
|
visited: set[str] = set()
|
||||||
|
for code in sorted(adjacency):
|
||||||
|
if code in visited:
|
||||||
|
continue
|
||||||
|
stack = [code]
|
||||||
|
component: set[str] = set()
|
||||||
|
while stack:
|
||||||
|
current = stack.pop()
|
||||||
|
if current in component:
|
||||||
|
continue
|
||||||
|
component.add(current)
|
||||||
|
visited.add(current)
|
||||||
|
stack.extend(adjacency.get(current, set()) - component)
|
||||||
|
members = sorted(component)
|
||||||
|
for member in members:
|
||||||
|
cluster_map[member] = members
|
||||||
|
return cluster_map
|
||||||
|
|
||||||
|
|
||||||
|
def get_process_cost_related_codes(support_dept_code: str | None) -> list[str]:
|
||||||
|
code = normalize_text(support_dept_code)
|
||||||
|
if not code:
|
||||||
|
return []
|
||||||
|
related_map = get_project_related_links_map()
|
||||||
|
return list(related_map.get(code, []))
|
||||||
|
|
||||||
|
|
||||||
def get_process_cost_available_years(source: str) -> list[int]:
|
def get_process_cost_available_years(source: str) -> list[int]:
|
||||||
normalized_source = normalize_text(source).lower()
|
normalized_source = normalize_text(source).lower()
|
||||||
if normalized_source == "wehago":
|
if normalized_source == "wehago":
|
||||||
@@ -5324,13 +5367,32 @@ def _get_project_contract_meta() -> dict[str, dict[str, Any]]:
|
|||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
|
WITH code_universe AS (
|
||||||
|
SELECT DISTINCT support_dept_code
|
||||||
|
FROM project_contract_info
|
||||||
|
WHERE COALESCE(support_dept_code, '') <> ''
|
||||||
|
UNION
|
||||||
|
SELECT DISTINCT support_dept_code
|
||||||
|
FROM project_status
|
||||||
|
WHERE COALESCE(support_dept_code, '') <> ''
|
||||||
|
UNION
|
||||||
|
SELECT DISTINCT support_dept_code
|
||||||
|
FROM project_basic_info
|
||||||
|
WHERE COALESCE(support_dept_code, '') <> ''
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(support_dept_code, '') AS support_dept_code,
|
COALESCE(u.support_dept_code, '') AS support_dept_code,
|
||||||
COALESCE(support_dept_name, '') AS support_dept_name,
|
COALESCE(c.support_dept_name, p.support_dept_name, b.support_dept_name, '') AS support_dept_name,
|
||||||
COALESCE(hanmac_contract_amount, 0) AS hanmac_contract_amount,
|
COALESCE(c.hanmac_contract_amount, 0) AS hanmac_contract_amount,
|
||||||
COALESCE(client_name, '') AS client_name
|
COALESCE(c.client_name, '') AS client_name,
|
||||||
FROM project_contract_info
|
COALESCE(p.expected_as_cost, b.expected_as_cost, 0) AS expected_as_cost
|
||||||
WHERE COALESCE(support_dept_code, '') <> ''
|
FROM code_universe AS u
|
||||||
|
LEFT JOIN project_contract_info AS c
|
||||||
|
ON c.support_dept_code = u.support_dept_code
|
||||||
|
LEFT JOIN project_status AS p
|
||||||
|
ON p.support_dept_code = u.support_dept_code
|
||||||
|
LEFT JOIN project_basic_info AS b
|
||||||
|
ON b.support_dept_code = u.support_dept_code
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
).mappings().all()
|
).mappings().all()
|
||||||
@@ -5343,9 +5405,53 @@ def _get_project_contract_meta() -> dict[str, dict[str, Any]]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_process_cost_project_options(source: str, selected_year: int | None) -> list[dict[str, Any]]:
|
def _get_hanmac_process_cost_tx_by_code(selected_year: int | None) -> dict[str, dict[str, Any]]:
|
||||||
|
year_clause = ""
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if selected_year:
|
||||||
|
year_clause = "AND year = :selected_year"
|
||||||
|
params["selected_year"] = selected_year
|
||||||
|
with engine.begin() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
text(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
COALESCE(support_dept_code, '') AS support_dept_code,
|
||||||
|
MAX(COALESCE(support_dept_name, '')) AS support_dept_name,
|
||||||
|
SUM(CASE WHEN {REVENUE_SQL} THEN amount ELSE 0 END) AS revenue_amount,
|
||||||
|
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN amount ELSE 0 END) AS expense_amount,
|
||||||
|
COUNT(DISTINCT CASE WHEN COALESCE(voucher_number, '') <> '' THEN voucher_number END) AS voucher_count,
|
||||||
|
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
||||||
|
FROM transactions
|
||||||
|
WHERE COALESCE(support_dept_code, '') <> ''
|
||||||
|
AND support_dept_code NOT IN ('ZZZZZZ')
|
||||||
|
AND COALESCE(support_dept_name, '') <> ''
|
||||||
|
AND support_dept_name NOT IN ('공통', '경영지원부', '기술개발센터', '임원실', '기술개발부', '총괄기획실')
|
||||||
|
{year_clause}
|
||||||
|
GROUP BY support_dept_code
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
params,
|
||||||
|
).mappings().all()
|
||||||
|
result: dict[str, dict[str, Any]] = {}
|
||||||
|
for row in rows:
|
||||||
|
code = normalize_text(row.get("support_dept_code"))
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
result[code] = {
|
||||||
|
"support_dept_name": normalize_text(row.get("support_dept_name")),
|
||||||
|
"revenue_amount": normalize_amount(row.get("revenue_amount")),
|
||||||
|
"expense_amount": normalize_amount(row.get("expense_amount")),
|
||||||
|
"voucher_count": int(row.get("voucher_count") or 0),
|
||||||
|
"last_posting_date": normalize_text(row.get("last_posting_date")),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_process_cost_project_options(source: str, selected_year: int | None, include_related: bool = False) -> list[dict[str, Any]]:
|
||||||
normalized_source = normalize_text(source).lower()
|
normalized_source = normalize_text(source).lower()
|
||||||
contract_meta = _get_project_contract_meta()
|
contract_meta = _get_project_contract_meta()
|
||||||
|
billing_summary_map = get_project_billing_summary_map() if normalized_source != "wehago" else {}
|
||||||
|
|
||||||
if normalized_source == "wehago":
|
if normalized_source == "wehago":
|
||||||
init_wehago_compare_db(engine)
|
init_wehago_compare_db(engine)
|
||||||
@@ -5391,34 +5497,26 @@ def get_process_cost_project_options(source: str, selected_year: int | None) ->
|
|||||||
params,
|
params,
|
||||||
).mappings().all()
|
).mappings().all()
|
||||||
else:
|
else:
|
||||||
year_clause = ""
|
tx_by_code = _get_hanmac_process_cost_tx_by_code(selected_year)
|
||||||
params = {}
|
universe_codes = sorted(set(contract_meta) | set(billing_summary_map) | set(tx_by_code))
|
||||||
if selected_year:
|
rows = []
|
||||||
year_clause = "AND year = :selected_year"
|
for code in universe_codes:
|
||||||
params["selected_year"] = selected_year
|
current_tx = tx_by_code.get(code, {})
|
||||||
with engine.begin() as conn:
|
expense_amount = normalize_amount(current_tx.get("expense_amount"))
|
||||||
rows = conn.execute(
|
revenue_amount = normalize_amount(current_tx.get("revenue_amount"))
|
||||||
text(
|
voucher_count = int(current_tx.get("voucher_count", 0) or 0)
|
||||||
f"""
|
last_posting_date = normalize_text(current_tx.get("last_posting_date"))
|
||||||
SELECT
|
tx_name = normalize_text(current_tx.get("support_dept_name"))
|
||||||
COALESCE(support_dept_code, '') AS support_dept_code,
|
rows.append(
|
||||||
MAX(COALESCE(support_dept_name, '')) AS support_dept_name,
|
{
|
||||||
SUM(CASE WHEN {REVENUE_SQL} THEN amount ELSE 0 END) AS revenue_amount,
|
"support_dept_code": code,
|
||||||
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN amount ELSE 0 END) AS expense_amount,
|
"support_dept_name": tx_name,
|
||||||
COUNT(DISTINCT COALESCE(voucher_number, '')) AS voucher_count,
|
"revenue_amount": revenue_amount,
|
||||||
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
"expense_amount": expense_amount,
|
||||||
FROM transactions
|
"voucher_count": voucher_count,
|
||||||
WHERE COALESCE(support_dept_code, '') <> ''
|
"last_posting_date": last_posting_date,
|
||||||
AND support_dept_code NOT IN ('ZZZZZZ')
|
}
|
||||||
AND COALESCE(support_dept_name, '') <> ''
|
)
|
||||||
AND support_dept_name NOT IN ('공통', '경영지원부', '기술개발센터', '임원실', '기술개발부', '총괄기획실')
|
|
||||||
{year_clause}
|
|
||||||
GROUP BY support_dept_code
|
|
||||||
ORDER BY expense_amount DESC, support_dept_code
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
params,
|
|
||||||
).mappings().all()
|
|
||||||
|
|
||||||
result: list[dict[str, Any]] = []
|
result: list[dict[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -5426,6 +5524,7 @@ def get_process_cost_project_options(source: str, selected_year: int | None) ->
|
|||||||
if not code:
|
if not code:
|
||||||
continue
|
continue
|
||||||
contract_row = contract_meta.get(code, {})
|
contract_row = contract_meta.get(code, {})
|
||||||
|
billing_row = billing_summary_map.get(code, {})
|
||||||
contract_amount = normalize_amount(contract_row.get("hanmac_contract_amount"))
|
contract_amount = normalize_amount(contract_row.get("hanmac_contract_amount"))
|
||||||
revenue_amount = normalize_amount(row.get("revenue_amount"))
|
revenue_amount = normalize_amount(row.get("revenue_amount"))
|
||||||
expense_amount = normalize_amount(row.get("expense_amount"))
|
expense_amount = normalize_amount(row.get("expense_amount"))
|
||||||
@@ -5433,8 +5532,8 @@ def get_process_cost_project_options(source: str, selected_year: int | None) ->
|
|||||||
result.append(
|
result.append(
|
||||||
{
|
{
|
||||||
"support_dept_code": code,
|
"support_dept_code": code,
|
||||||
"support_dept_name": normalize_text(contract_row.get("support_dept_name")) or normalize_text(row.get("support_dept_name")) or code,
|
"support_dept_name": normalize_text(contract_row.get("support_dept_name")) or normalize_text(billing_row.get("support_dept_name")) or normalize_text(row.get("support_dept_name")) or code,
|
||||||
"client_name": normalize_text(contract_row.get("client_name")),
|
"client_name": normalize_text(contract_row.get("client_name")) or normalize_text(billing_row.get("client_name")),
|
||||||
"contract_amount": contract_amount,
|
"contract_amount": contract_amount,
|
||||||
"revenue_amount": revenue_amount,
|
"revenue_amount": revenue_amount,
|
||||||
"expense_amount": expense_amount,
|
"expense_amount": expense_amount,
|
||||||
@@ -5444,11 +5543,16 @@ def get_process_cost_project_options(source: str, selected_year: int | None) ->
|
|||||||
"last_posting_date": normalize_text(row.get("last_posting_date")),
|
"last_posting_date": normalize_text(row.get("last_posting_date")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result.sort(key=lambda item: (-normalize_amount(item.get("expense_amount")), item.get("support_dept_code", "")))
|
result.sort(key=lambda item: (-normalize_amount(item.get("expense_amount")), -normalize_amount(item.get("revenue_amount")), item.get("support_dept_code", "")))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_process_cost_project_detail(source: str, selected_year: int | None, support_dept_code: str) -> dict[str, Any]:
|
def get_process_cost_project_detail(
|
||||||
|
source: str,
|
||||||
|
selected_year: int | None,
|
||||||
|
support_dept_code: str,
|
||||||
|
include_related: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
code = normalize_text(support_dept_code)
|
code = normalize_text(support_dept_code)
|
||||||
if not code:
|
if not code:
|
||||||
return {
|
return {
|
||||||
@@ -5460,11 +5564,15 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
}
|
}
|
||||||
|
|
||||||
normalized_source = normalize_text(source).lower()
|
normalized_source = normalize_text(source).lower()
|
||||||
contract_meta = _get_project_contract_meta().get(code, {})
|
contract_meta_map = _get_project_contract_meta()
|
||||||
|
contract_meta = contract_meta_map.get(code, {})
|
||||||
|
related_clusters = _build_process_cost_related_clusters() if include_related else {}
|
||||||
|
cluster_codes = related_clusters.get(code, [code]) if include_related else [code]
|
||||||
if normalized_source == "wehago":
|
if normalized_source == "wehago":
|
||||||
init_wehago_compare_db(engine)
|
init_wehago_compare_db(engine)
|
||||||
|
in_clause, code_params = build_in_clause("process_cost_wehago_code", cluster_codes)
|
||||||
year_clause = ""
|
year_clause = ""
|
||||||
params: dict[str, Any] = {"support_dept_code": code}
|
params: dict[str, Any] = dict(code_params)
|
||||||
if selected_year:
|
if selected_year:
|
||||||
year_clause = "AND fiscal_year = :selected_year"
|
year_clause = "AND fiscal_year = :selected_year"
|
||||||
params["selected_year"] = selected_year
|
params["selected_year"] = selected_year
|
||||||
@@ -5490,7 +5598,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
COUNT(DISTINCT CASE WHEN COALESCE(confirmed_no, '') <> '' THEN confirmed_no ELSE draft_no END) AS voucher_count,
|
COUNT(DISTINCT CASE WHEN COALESCE(confirmed_no, '') <> '' THEN confirmed_no ELSE draft_no END) AS voucher_count,
|
||||||
MAX(COALESCE(proof_date, '')) AS last_posting_date
|
MAX(COALESCE(proof_date, '')) AS last_posting_date
|
||||||
FROM wehago_voucher_rows
|
FROM wehago_voucher_rows
|
||||||
WHERE support_dept_code = :support_dept_code
|
WHERE support_dept_code IN ({in_clause})
|
||||||
{year_clause}
|
{year_clause}
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
@@ -5506,7 +5614,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
COUNT(*) AS row_count,
|
COUNT(*) AS row_count,
|
||||||
MAX(COALESCE(proof_date, '')) AS last_posting_date
|
MAX(COALESCE(proof_date, '')) AS last_posting_date
|
||||||
FROM wehago_voucher_rows
|
FROM wehago_voucher_rows
|
||||||
WHERE support_dept_code = :support_dept_code
|
WHERE support_dept_code IN ({in_clause})
|
||||||
AND (account_code LIKE '5%' OR account_code LIKE '6%')
|
AND (account_code LIKE '5%' OR account_code LIKE '6%')
|
||||||
{year_clause}
|
{year_clause}
|
||||||
GROUP BY account_code, account_name
|
GROUP BY account_code, account_name
|
||||||
@@ -5524,7 +5632,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
SUM(CASE WHEN account_code LIKE '401101%' OR account_code LIKE '401102%' THEN {amount_expr} ELSE 0 END) AS revenue_amount,
|
SUM(CASE WHEN account_code LIKE '401101%' OR account_code LIKE '401102%' THEN {amount_expr} ELSE 0 END) AS revenue_amount,
|
||||||
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN {amount_expr} ELSE 0 END) AS expense_amount
|
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN {amount_expr} ELSE 0 END) AS expense_amount
|
||||||
FROM wehago_voucher_rows
|
FROM wehago_voucher_rows
|
||||||
WHERE support_dept_code = :support_dept_code
|
WHERE support_dept_code IN ({in_clause})
|
||||||
{year_clause}
|
{year_clause}
|
||||||
AND LENGTH(COALESCE(proof_date, '')) >= 7
|
AND LENGTH(COALESCE(proof_date, '')) >= 7
|
||||||
GROUP BY month_label
|
GROUP BY month_label
|
||||||
@@ -5535,8 +5643,9 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
params,
|
params,
|
||||||
).mappings().all()
|
).mappings().all()
|
||||||
else:
|
else:
|
||||||
|
in_clause, code_params = build_in_clause("process_cost_code", cluster_codes)
|
||||||
year_clause = ""
|
year_clause = ""
|
||||||
params = {"support_dept_code": code}
|
params = dict(code_params)
|
||||||
if selected_year:
|
if selected_year:
|
||||||
year_clause = "AND year = :selected_year"
|
year_clause = "AND year = :selected_year"
|
||||||
params["selected_year"] = selected_year
|
params["selected_year"] = selected_year
|
||||||
@@ -5555,7 +5664,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
COUNT(DISTINCT COALESCE(voucher_number, '')) AS voucher_count,
|
COUNT(DISTINCT COALESCE(voucher_number, '')) AS voucher_count,
|
||||||
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE support_dept_code = :support_dept_code
|
WHERE support_dept_code IN ({in_clause})
|
||||||
{year_clause}
|
{year_clause}
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
@@ -5571,7 +5680,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
COUNT(*) AS row_count,
|
COUNT(*) AS row_count,
|
||||||
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE support_dept_code = :support_dept_code
|
WHERE support_dept_code IN ({in_clause})
|
||||||
AND (account_code LIKE '5%' OR account_code LIKE '6%')
|
AND (account_code LIKE '5%' OR account_code LIKE '6%')
|
||||||
{year_clause}
|
{year_clause}
|
||||||
GROUP BY account_code, account_name
|
GROUP BY account_code, account_name
|
||||||
@@ -5589,7 +5698,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
SUM(CASE WHEN {REVENUE_SQL} THEN amount ELSE 0 END) AS revenue_amount,
|
SUM(CASE WHEN {REVENUE_SQL} THEN amount ELSE 0 END) AS revenue_amount,
|
||||||
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN amount ELSE 0 END) AS expense_amount
|
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN amount ELSE 0 END) AS expense_amount
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE support_dept_code = :support_dept_code
|
WHERE support_dept_code IN ({in_clause})
|
||||||
{year_clause}
|
{year_clause}
|
||||||
AND year IS NOT NULL
|
AND year IS NOT NULL
|
||||||
AND month IS NOT NULL
|
AND month IS NOT NULL
|
||||||
@@ -5602,7 +5711,12 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
).mappings().all()
|
).mappings().all()
|
||||||
|
|
||||||
summary_row = dict(summary) if summary else {}
|
summary_row = dict(summary) if summary else {}
|
||||||
contract_amount = normalize_amount(contract_meta.get("hanmac_contract_amount"))
|
if include_related:
|
||||||
|
contract_amount = sum(normalize_amount(contract_meta_map.get(member, {}).get("hanmac_contract_amount")) for member in cluster_codes)
|
||||||
|
as_cost_amount = sum(normalize_amount(contract_meta_map.get(member, {}).get("expected_as_cost")) for member in cluster_codes)
|
||||||
|
else:
|
||||||
|
contract_amount = normalize_amount(contract_meta.get("hanmac_contract_amount"))
|
||||||
|
as_cost_amount = normalize_amount(contract_meta.get("expected_as_cost"))
|
||||||
revenue_amount = normalize_amount(summary_row.get("revenue_amount"))
|
revenue_amount = normalize_amount(summary_row.get("revenue_amount"))
|
||||||
expense_amount = normalize_amount(summary_row.get("expense_amount"))
|
expense_amount = normalize_amount(summary_row.get("expense_amount"))
|
||||||
labor_amount = normalize_amount(summary_row.get("labor_amount"))
|
labor_amount = normalize_amount(summary_row.get("labor_amount"))
|
||||||
@@ -5615,7 +5729,8 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
phase_rows = [
|
phase_rows = [
|
||||||
{"phase": "직접인건비", "target_amount": target_base * 0.30, "actual_amount": labor_amount},
|
{"phase": "직접인건비", "target_amount": target_base * 0.30, "actual_amount": labor_amount},
|
||||||
{"phase": "외주비", "target_amount": target_base * 0.32, "actual_amount": outsourcing_amount},
|
{"phase": "외주비", "target_amount": target_base * 0.32, "actual_amount": outsourcing_amount},
|
||||||
{"phase": "설계경비", "target_amount": target_base * 0.23, "actual_amount": design_cost_amount},
|
{"phase": "제경비", "target_amount": target_base * 0.23, "actual_amount": design_cost_amount},
|
||||||
|
{"phase": "A/S비", "target_amount": as_cost_amount, "actual_amount": as_cost_amount},
|
||||||
{"phase": "판관비", "target_amount": target_base * 0.15, "actual_amount": sga_amount},
|
{"phase": "판관비", "target_amount": target_base * 0.15, "actual_amount": sga_amount},
|
||||||
]
|
]
|
||||||
for row in phase_rows:
|
for row in phase_rows:
|
||||||
@@ -5665,6 +5780,7 @@ def get_process_cost_project_detail(source: str, selected_year: int | None, supp
|
|||||||
"last_posting_date": normalize_text(summary_row.get("last_posting_date")),
|
"last_posting_date": normalize_text(summary_row.get("last_posting_date")),
|
||||||
"voucher_count": int(summary_row.get("voucher_count") or 0),
|
"voucher_count": int(summary_row.get("voucher_count") or 0),
|
||||||
"row_count": int(summary_row.get("row_count") or 0),
|
"row_count": int(summary_row.get("row_count") or 0),
|
||||||
|
"included_codes": cluster_codes if include_related else [code],
|
||||||
},
|
},
|
||||||
"phase_rows": phase_rows,
|
"phase_rows": phase_rows,
|
||||||
"account_rows": normalized_accounts,
|
"account_rows": normalized_accounts,
|
||||||
@@ -5684,6 +5800,7 @@ def render_process_cost_page(
|
|||||||
source: str | None = None,
|
source: str | None = None,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
code: str | None = None,
|
code: str | None = None,
|
||||||
|
include_related: bool = False,
|
||||||
message: str = "",
|
message: str = "",
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
init_db()
|
init_db()
|
||||||
@@ -5693,23 +5810,30 @@ def render_process_cost_page(
|
|||||||
normalized_source = "hanmac"
|
normalized_source = "hanmac"
|
||||||
|
|
||||||
years = get_process_cost_available_years(normalized_source)
|
years = get_process_cost_available_years(normalized_source)
|
||||||
selected_year = year if year in years else (max(years) if years else None)
|
selected_year = year if year in years else None
|
||||||
project_options = get_process_cost_project_options(normalized_source, selected_year)
|
project_options = get_process_cost_project_options(normalized_source, selected_year, include_related=include_related)
|
||||||
selected_code = normalize_text(code)
|
selected_code = normalize_text(code)
|
||||||
if selected_code and not any(item["support_dept_code"] == selected_code for item in project_options):
|
if selected_code and not any(item["support_dept_code"] == selected_code for item in project_options):
|
||||||
selected_code = ""
|
selected_code = ""
|
||||||
if not selected_code and project_options:
|
if not selected_code and project_options:
|
||||||
selected_code = project_options[0]["support_dept_code"]
|
selected_code = project_options[0]["support_dept_code"]
|
||||||
|
|
||||||
detail = get_process_cost_project_detail(normalized_source, selected_year, selected_code)
|
detail = get_process_cost_project_detail(
|
||||||
|
normalized_source,
|
||||||
|
selected_year,
|
||||||
|
selected_code,
|
||||||
|
include_related=include_related,
|
||||||
|
)
|
||||||
context = {
|
context = {
|
||||||
**base_context(request, message),
|
**base_context(request, message),
|
||||||
"process_cost_source": normalized_source,
|
"process_cost_source": normalized_source,
|
||||||
"process_cost_years": years,
|
"process_cost_years": years,
|
||||||
"process_cost_selected_year": selected_year,
|
"process_cost_selected_year": selected_year,
|
||||||
"process_cost_selected_code": selected_code,
|
"process_cost_selected_code": selected_code,
|
||||||
|
"process_cost_include_related": include_related,
|
||||||
"process_cost_projects": project_options,
|
"process_cost_projects": project_options,
|
||||||
"process_cost_detail": detail,
|
"process_cost_detail": detail,
|
||||||
|
"process_cost_related_codes": get_process_cost_related_codes(selected_code),
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse(request, "process_cost.html", context)
|
return templates.TemplateResponse(request, "process_cost.html", context)
|
||||||
|
|
||||||
@@ -7092,6 +7216,7 @@ async def process_cost(
|
|||||||
source: str | None = None,
|
source: str | None = None,
|
||||||
year: str | None = None,
|
year: str | None = None,
|
||||||
code: str | None = None,
|
code: str | None = None,
|
||||||
|
include_related: str | None = None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
return render_process_cost_page(
|
return render_process_cost_page(
|
||||||
@@ -7099,6 +7224,7 @@ async def process_cost(
|
|||||||
source=source,
|
source=source,
|
||||||
year=parse_optional_year(year),
|
year=parse_optional_year(year),
|
||||||
code=code,
|
code=code,
|
||||||
|
include_related=normalize_text(include_related) in {"1", "true", "y", "yes", "on"},
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("프로세스 원가 페이지 에러: %s", exc)
|
logger.exception("프로세스 원가 페이지 에러: %s", exc)
|
||||||
|
|||||||
+244
-16
@@ -48,6 +48,13 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pc-tab-row {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.pc-filter {
|
.pc-filter {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -144,6 +151,83 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pc-related-box {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-related-box h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-chip-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-chip button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
min-height: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-related-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-related-list {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-related-list.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-related-item {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
padding: 9px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-related-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
.pc-block {
|
.pc-block {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -309,23 +393,28 @@
|
|||||||
{% set overview = detail.overview or {} %}
|
{% set overview = detail.overview or {} %}
|
||||||
{% set diagnostics = detail.diagnostics or {} %}
|
{% set diagnostics = detail.diagnostics or {} %}
|
||||||
<section class="panel pc-shell">
|
<section class="panel pc-shell">
|
||||||
<div class="section-title" style="margin-bottom: 0;">
|
|
||||||
<h2>프로세스 원가 관리</h2>
|
|
||||||
<p>토목설계회사 기준 초안 (직접인건비/외주비/설계경비/판관비 중심)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pc-toolbar">
|
<div class="pc-toolbar">
|
||||||
<div class="pc-source-tabs">
|
<div class="pc-tab-row">
|
||||||
<a href="/process-cost?source=hanmac{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}" class="{% if process_cost_source == 'hanmac' %}active{% endif %}">Hanmac ERP</a>
|
<div class="pc-source-tabs">
|
||||||
<a href="/process-cost?source=wehago{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}" class="{% if process_cost_source == 'wehago' %}active{% endif %}">WEHAGO</a>
|
<a href="/process-cost?source=hanmac{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}{% if process_cost_include_related %}&include_related=1{% endif %}" class="{% if process_cost_source == 'hanmac' %}active{% endif %}">Hanmac ERP</a>
|
||||||
|
<a href="/process-cost?source=wehago{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}{% if process_cost_include_related %}&include_related=1{% endif %}" class="{% if process_cost_source == 'wehago' %}active{% endif %}">WEHAGO</a>
|
||||||
|
</div>
|
||||||
|
<div class="pc-source-tabs">
|
||||||
|
<a href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}" class="{% if not process_cost_include_related %}active{% endif %}">연계 미반영</a>
|
||||||
|
<a href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}&include_related=1" class="{% if process_cost_include_related %}active{% endif %}">연계 반영</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="get" action="/process-cost" class="pc-filter">
|
<form method="get" action="/process-cost" class="pc-filter">
|
||||||
<input type="hidden" name="source" value="{{ process_cost_source }}">
|
<input type="hidden" name="source" value="{{ process_cost_source }}">
|
||||||
<input type="hidden" name="code" value="{{ process_cost_selected_code }}">
|
<input type="hidden" name="code" value="{{ process_cost_selected_code }}">
|
||||||
|
{% if process_cost_include_related %}
|
||||||
|
<input type="hidden" name="include_related" value="1">
|
||||||
|
{% endif %}
|
||||||
<select name="year" aria-label="연도 선택">
|
<select name="year" aria-label="연도 선택">
|
||||||
{% if not process_cost_years %}
|
{% if not process_cost_years %}
|
||||||
<option value="">연도 없음</option>
|
<option value="">연도 없음</option>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<option value="" {% if not process_cost_selected_year %}selected{% endif %}>전체 기간</option>
|
||||||
{% for y in process_cost_years|sort(reverse=True) %}
|
{% for y in process_cost_years|sort(reverse=True) %}
|
||||||
<option value="{{ y }}" {% if process_cost_selected_year == y %}selected{% endif %}>{{ y }}</option>
|
<option value="{{ y }}" {% if process_cost_selected_year == y %}selected{% endif %}>{{ y }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -344,7 +433,7 @@
|
|||||||
<div class="pc-project-list" id="projectList">
|
<div class="pc-project-list" id="projectList">
|
||||||
{% for item in process_cost_projects %}
|
{% for item in process_cost_projects %}
|
||||||
<a
|
<a
|
||||||
href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}&code={{ item.support_dept_code }}"
|
href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_year %}&year={{ process_cost_selected_year }}{% endif %}&code={{ item.support_dept_code }}{% if process_cost_include_related %}&include_related=1{% endif %}"
|
||||||
class="pc-project-item {% if process_cost_selected_code == item.support_dept_code %}active{% endif %}"
|
class="pc-project-item {% if process_cost_selected_code == item.support_dept_code %}active{% endif %}"
|
||||||
data-project-search="{{ (item.support_dept_code ~ ' ' ~ item.support_dept_name ~ ' ' ~ (item.client_name or ''))|lower }}"
|
data-project-search="{{ (item.support_dept_code ~ ' ' ~ item.support_dept_name ~ ' ' ~ (item.client_name or ''))|lower }}"
|
||||||
title="{{ item.support_dept_code }} {{ item.support_dept_name }}"
|
title="{{ item.support_dept_code }} {{ item.support_dept_name }}"
|
||||||
@@ -370,12 +459,10 @@
|
|||||||
<article class="pc-card">
|
<article class="pc-card">
|
||||||
<span class="label">계약금액</span>
|
<span class="label">계약금액</span>
|
||||||
<strong class="value">{{ "{:,.0f}".format(overview.contract_amount|default(0)) }}원</strong>
|
<strong class="value">{{ "{:,.0f}".format(overview.contract_amount|default(0)) }}원</strong>
|
||||||
<span class="meta">{{ overview.support_dept_name or '-' }}</span>
|
|
||||||
</article>
|
</article>
|
||||||
<article class="pc-card">
|
<article class="pc-card">
|
||||||
<span class="label">누적 수익</span>
|
<span class="label">누적 수익</span>
|
||||||
<strong class="value">{{ "{:,.0f}".format(overview.revenue_amount|default(0)) }}원</strong>
|
<strong class="value">{{ "{:,.0f}".format(overview.revenue_amount|default(0)) }}원</strong>
|
||||||
<span class="meta">최근 증빙일 {{ overview.last_posting_date or '-' }}</span>
|
|
||||||
</article>
|
</article>
|
||||||
<article class="pc-card">
|
<article class="pc-card">
|
||||||
<span class="label">누적 비용</span>
|
<span class="label">누적 비용</span>
|
||||||
@@ -389,9 +476,26 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pc-note">
|
<section class="pc-related-box">
|
||||||
설계회사 프로세스 원가 관리 초안: 목표원가(계약금액 기준 85%)와 실제 집행을 비교해 과다/과소 집행 구간을 빠르게 파악합니다.
|
<h3>연계 프로젝트</h3>
|
||||||
</div>
|
<div class="pc-chip-row" id="relatedChipRow">
|
||||||
|
{% if process_cost_selected_code %}
|
||||||
|
<span class="pc-chip">{{ process_cost_selected_code }} 기준</span>
|
||||||
|
{% endif %}
|
||||||
|
{% for related_code in process_cost_related_codes or [] %}
|
||||||
|
<span class="pc-chip" data-related-code="{{ related_code }}">
|
||||||
|
<span>{{ related_code }}</span>
|
||||||
|
<button type="button" class="pc-related-remove" data-related-remove="{{ related_code }}">x</button>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="pc-related-form">
|
||||||
|
<input type="text" id="relatedSearchInput" placeholder="추가할 프로젝트 코드/명 검색" {% if not process_cost_selected_code %}disabled{% endif %}>
|
||||||
|
<button type="button" class="button-secondary" id="relatedSaveButton" {% if not process_cost_selected_code %}disabled{% endif %}>저장</button>
|
||||||
|
<button type="button" class="button-secondary" id="relatedResetButton" {% if not process_cost_selected_code %}disabled{% endif %}>초기화</button>
|
||||||
|
</div>
|
||||||
|
<div class="pc-related-list" id="relatedSuggestList"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="pc-block">
|
<section class="pc-block">
|
||||||
<h3>설계 프로세스별 원가 현황</h3>
|
<h3>설계 프로세스별 원가 현황</h3>
|
||||||
@@ -474,7 +578,7 @@
|
|||||||
<div class="pc-bar"><span data-rate="{{ diagnostics.outsourcing_ratio|default(0) }}"></span></div>
|
<div class="pc-bar"><span data-rate="{{ diagnostics.outsourcing_ratio|default(0) }}"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pc-kpi-row">
|
<div class="pc-kpi-row">
|
||||||
<div class="pc-kpi-head"><span>설계경비 비중</span><strong>{{ "{:.1f}".format(diagnostics.design_cost_ratio|default(0)) }}%</strong></div>
|
<div class="pc-kpi-head"><span>제경비 비중</span><strong>{{ "{:.1f}".format(diagnostics.design_cost_ratio|default(0)) }}%</strong></div>
|
||||||
<div class="pc-bar"><span data-rate="{{ diagnostics.design_cost_ratio|default(0) }}"></span></div>
|
<div class="pc-bar"><span data-rate="{{ diagnostics.design_cost_ratio|default(0) }}"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pc-kpi-row">
|
<div class="pc-kpi-row">
|
||||||
@@ -486,10 +590,11 @@
|
|||||||
<section class="pc-kpi">
|
<section class="pc-kpi">
|
||||||
<h3>데이터 현황</h3>
|
<h3>데이터 현황</h3>
|
||||||
<div class="pc-kpi-head"><span>선택 소스</span><strong>{% if process_cost_source == 'wehago' %}WEHAGO{% else %}Hanmac ERP{% endif %}</strong></div>
|
<div class="pc-kpi-head"><span>선택 소스</span><strong>{% if process_cost_source == 'wehago' %}WEHAGO{% else %}Hanmac ERP{% endif %}</strong></div>
|
||||||
<div class="pc-kpi-head"><span>연도</span><strong>{{ process_cost_selected_year or '-' }}</strong></div>
|
<div class="pc-kpi-head"><span>연도</span><strong>{{ process_cost_selected_year or '전체 기간' }}</strong></div>
|
||||||
<div class="pc-kpi-head"><span>전표 건수</span><strong>{{ "{:,}".format(overview.voucher_count|default(0)) }}</strong></div>
|
<div class="pc-kpi-head"><span>전표 건수</span><strong>{{ "{:,}".format(overview.voucher_count|default(0)) }}</strong></div>
|
||||||
<div class="pc-kpi-head"><span>행 건수</span><strong>{{ "{:,}".format(overview.row_count|default(0)) }}</strong></div>
|
<div class="pc-kpi-head"><span>행 건수</span><strong>{{ "{:,}".format(overview.row_count|default(0)) }}</strong></div>
|
||||||
<div class="pc-kpi-head"><span>실행률</span><strong>{{ "{:.1f}".format(overview.execution_rate|default(0)) }}%</strong></div>
|
<div class="pc-kpi-head"><span>실행률</span><strong>{{ "{:.1f}".format(overview.execution_rate|default(0)) }}%</strong></div>
|
||||||
|
<div class="pc-kpi-head"><span>연계 반영</span><strong>{% if process_cost_include_related %}반영{% else %}미반영{% endif %}</strong></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pc-mini-chart">
|
<section class="pc-mini-chart">
|
||||||
@@ -504,6 +609,9 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
|
const processCostProjects = {{ process_cost_projects | tojson }};
|
||||||
|
const selectedCode = {{ process_cost_selected_code | tojson }};
|
||||||
|
const initialRelatedCodes = {{ (process_cost_related_codes or []) | tojson }};
|
||||||
const searchInput = document.getElementById("projectSearchInput");
|
const searchInput = document.getElementById("projectSearchInput");
|
||||||
const list = document.getElementById("projectList");
|
const list = document.getElementById("projectList");
|
||||||
if (searchInput && list) {
|
if (searchInput && list) {
|
||||||
@@ -522,6 +630,126 @@
|
|||||||
bar.style.width = `${width}%`;
|
bar.style.width = `${width}%`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const relatedSearchInput = document.getElementById("relatedSearchInput");
|
||||||
|
const relatedSuggestList = document.getElementById("relatedSuggestList");
|
||||||
|
const relatedChipRow = document.getElementById("relatedChipRow");
|
||||||
|
const relatedSaveButton = document.getElementById("relatedSaveButton");
|
||||||
|
const relatedResetButton = document.getElementById("relatedResetButton");
|
||||||
|
const relatedEmptyState = document.getElementById("relatedEmptyState");
|
||||||
|
const relatedSet = new Set(initialRelatedCodes || []);
|
||||||
|
|
||||||
|
function renderRelatedChips() {
|
||||||
|
if (!relatedChipRow) return;
|
||||||
|
const fixedHead = selectedCode ? `<span class="pc-chip">${selectedCode} 기준</span>` : "";
|
||||||
|
const chips = [...relatedSet].sort().map((code) => `
|
||||||
|
<span class="pc-chip" data-related-code="${code}">
|
||||||
|
<span>${code}</span>
|
||||||
|
<button type="button" class="pc-related-remove" data-related-remove="${code}">x</button>
|
||||||
|
</span>
|
||||||
|
`).join("");
|
||||||
|
const empty = "";
|
||||||
|
relatedChipRow.innerHTML = fixedHead + chips + empty;
|
||||||
|
relatedChipRow.querySelectorAll("[data-related-remove]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const code = button.dataset.relatedRemove || "";
|
||||||
|
if (code) {
|
||||||
|
relatedSet.delete(code);
|
||||||
|
renderRelatedChips();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRelatedSuggestions(keyword) {
|
||||||
|
if (!relatedSuggestList) return;
|
||||||
|
const normalized = (keyword || "").trim().toLowerCase();
|
||||||
|
if (!normalized || !selectedCode) {
|
||||||
|
relatedSuggestList.classList.remove("active");
|
||||||
|
relatedSuggestList.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = processCostProjects
|
||||||
|
.filter((item) => item.support_dept_code !== selectedCode)
|
||||||
|
.filter((item) => !relatedSet.has(item.support_dept_code))
|
||||||
|
.filter((item) => `${item.support_dept_code} ${item.support_dept_name || ""} ${item.client_name || ""}`.toLowerCase().includes(normalized))
|
||||||
|
.slice(0, 30);
|
||||||
|
if (!rows.length) {
|
||||||
|
relatedSuggestList.classList.remove("active");
|
||||||
|
relatedSuggestList.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
relatedSuggestList.innerHTML = rows.map((item) => `
|
||||||
|
<button type="button" class="pc-related-item" data-related-pick="${item.support_dept_code}">
|
||||||
|
${item.support_dept_code} ${item.support_dept_name || ""}
|
||||||
|
</button>
|
||||||
|
`).join("");
|
||||||
|
relatedSuggestList.classList.add("active");
|
||||||
|
relatedSuggestList.querySelectorAll("[data-related-pick]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const code = button.dataset.relatedPick || "";
|
||||||
|
if (code) {
|
||||||
|
relatedSet.add(code);
|
||||||
|
if (relatedSearchInput) relatedSearchInput.value = "";
|
||||||
|
relatedSuggestList.classList.remove("active");
|
||||||
|
relatedSuggestList.innerHTML = "";
|
||||||
|
renderRelatedChips();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedSearchInput) {
|
||||||
|
relatedSearchInput.addEventListener("input", () => {
|
||||||
|
renderRelatedSuggestions(relatedSearchInput.value || "");
|
||||||
|
});
|
||||||
|
relatedSearchInput.addEventListener("focus", () => {
|
||||||
|
renderRelatedSuggestions(relatedSearchInput.value || "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedResetButton) {
|
||||||
|
relatedResetButton.addEventListener("click", () => {
|
||||||
|
relatedSet.clear();
|
||||||
|
for (const code of initialRelatedCodes || []) {
|
||||||
|
relatedSet.add(code);
|
||||||
|
}
|
||||||
|
if (relatedSearchInput) relatedSearchInput.value = "";
|
||||||
|
if (relatedSuggestList) {
|
||||||
|
relatedSuggestList.classList.remove("active");
|
||||||
|
relatedSuggestList.innerHTML = "";
|
||||||
|
}
|
||||||
|
renderRelatedChips();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedSaveButton) {
|
||||||
|
relatedSaveButton.addEventListener("click", async () => {
|
||||||
|
if (!selectedCode) return;
|
||||||
|
relatedSaveButton.disabled = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/projects/related-links", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
base_code: selectedCode,
|
||||||
|
related_codes: [...relatedSet],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const payload = await response.json();
|
||||||
|
if (!response.ok || payload.error) {
|
||||||
|
throw new Error(payload.error || "연계 프로젝트 저장에 실패했습니다.");
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message || "연계 프로젝트 저장에 실패했습니다.");
|
||||||
|
} finally {
|
||||||
|
relatedSaveButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRelatedChips();
|
||||||
|
|
||||||
const monthlyRows = {{ (process_cost_detail.monthly_rows or []) | tojson }};
|
const monthlyRows = {{ (process_cost_detail.monthly_rows or []) | tojson }};
|
||||||
const svg = document.getElementById("processMonthlyChart");
|
const svg = document.getElementById("processMonthlyChart");
|
||||||
if (!svg || !Array.isArray(monthlyRows) || !monthlyRows.length) {
|
if (!svg || !Array.isArray(monthlyRows) || !monthlyRows.length) {
|
||||||
|
|||||||
@@ -1023,17 +1023,17 @@
|
|||||||
|
|
||||||
.analysis-root-panel {
|
.analysis-root-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 40;
|
z-index: 400;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
content-visibility: auto;
|
content-visibility: visible;
|
||||||
contain-intrinsic-size: 1200px;
|
contain: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-uncontracted-panel {
|
.project-uncontracted-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
content-visibility: auto;
|
content-visibility: visible;
|
||||||
contain-intrinsic-size: 960px;
|
contain: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uncontracted-toolbar {
|
.uncontracted-toolbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user