diff --git a/data.db b/data.db index 8e18fb9..24cb51d 100644 Binary files a/data.db and b/data.db differ diff --git a/main.py b/main.py index 27757e9..6c12ebd 100644 --- a/main.py +++ b/main.py @@ -4473,6 +4473,54 @@ def save_project_quick_links(session_id: str | None, codes: list[str]) -> None: ) +def get_process_cost_quick_links() -> list[str]: + with engine.begin() as conn: + rows = conn.execute( + text( + """ + SELECT support_dept_code + FROM project_quick_links + WHERE page_key = 'process_cost' + ORDER BY sort_order, updated_at DESC, support_dept_code + """ + ) + ).mappings().all() + return [normalize_text(row["support_dept_code"]) for row in rows if normalize_text(row["support_dept_code"])] + + +def save_process_cost_quick_links(codes: list[str]) -> None: + normalized_codes: list[str] = [] + for code in codes: + normalized_code = normalize_text(code) + if normalized_code and normalized_code not in normalized_codes: + normalized_codes.append(normalized_code) + with engine.begin() as conn: + conn.execute( + text( + """ + DELETE FROM project_quick_links + WHERE page_key = 'process_cost' + """ + ) + ) + for sort_order, support_dept_code in enumerate(normalized_codes): + conn.execute( + text( + """ + INSERT INTO project_quick_links ( + page_key, support_dept_code, sort_order, updated_at + ) VALUES ( + 'process_cost', :support_dept_code, :sort_order, CURRENT_TIMESTAMP + ) + """ + ), + { + "support_dept_code": support_dept_code, + "sort_order": sort_order, + }, + ) + + def get_project_uncontracted_classification_map() -> dict[str, str]: with engine.begin() as conn: rows = conn.execute( @@ -5332,34 +5380,53 @@ def get_process_cost_related_codes(support_dept_code: str | None) -> list[str]: def get_process_cost_available_years(source: str) -> list[int]: - normalized_source = normalize_text(source).lower() - if normalized_source == "wehago": - init_wehago_compare_db(engine) - with engine.begin() as conn: - rows = conn.execute( - text( - """ - SELECT DISTINCT fiscal_year - FROM wehago_voucher_rows - WHERE fiscal_year IS NOT NULL - ORDER BY fiscal_year - """ - ) - ).fetchall() - return [int(row[0]) for row in rows if row and row[0] is not None] + contract_meta = _get_project_contract_meta() + detected_years = { + year + for code in contract_meta.keys() + for year in [_extract_year_from_project_code(code)] + if year is not None + } + max_year = max(detected_years) if detected_years else datetime.now().year + min_year = 1994 + if max_year < min_year: + max_year = min_year + return list(range(min_year, max_year + 1)) - with engine.begin() as conn: - rows = conn.execute( - text( - """ - SELECT DISTINCT year - FROM transactions - WHERE year IS NOT NULL - ORDER BY year - """ - ) - ).fetchall() - return [int(row[0]) for row in rows if row and row[0] is not None] + +def _extract_year_from_project_code(value: Any) -> int | None: + code = normalize_text(value).upper() + if len(code) < 3: + return None + digits = "".join(ch for ch in code if ch.isdigit()) + if len(digits) < 2: + return None + year_2d = digits[:2] + if not year_2d.isdigit(): + return None + year_value = int(year_2d) + if year_value >= 94: + return 1900 + year_value + return 2000 + year_value + + +def _sort_process_cost_project_options(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + def sort_key(item: dict[str, Any]) -> str: + return normalize_text(item.get("support_dept_code")).upper() + + return sorted(items, key=sort_key) + + +def _get_process_cost_project_kind(code: Any) -> tuple[str, str]: + normalized = normalize_text(code).upper() + prefix = normalized[:1] + if prefix == "X": + return "X", "사전사업" + if prefix == "Y": + return "Y", "설계" + if prefix == "Z": + return "Z", "감리" + return "", "기타" def _get_project_contract_meta() -> dict[str, dict[str, Any]]: @@ -5385,7 +5452,9 @@ def _get_project_contract_meta() -> dict[str, dict[str, Any]]: COALESCE(c.support_dept_name, p.support_dept_name, b.support_dept_name, '') AS support_dept_name, COALESCE(c.hanmac_contract_amount, 0) AS hanmac_contract_amount, COALESCE(c.client_name, '') AS client_name, - COALESCE(p.expected_as_cost, b.expected_as_cost, 0) AS expected_as_cost + COALESCE(p.expected_as_cost, b.expected_as_cost, 0) AS expected_as_cost, + COALESCE(p.expected_sga_budget, b.expected_sga_budget, 0) AS expected_sga_budget, + COALESCE(p.project_start_date, b.project_start_date, '') AS project_start_date FROM code_universe AS u LEFT JOIN project_contract_info AS c ON c.support_dept_code = u.support_dept_code @@ -5405,12 +5474,117 @@ def _get_project_contract_meta() -> dict[str, dict[str, Any]]: return result +def _get_project_actual_sga_summary(codes: list[str]) -> dict[str, dict[str, Any]]: + normalized_codes = [normalize_text(code) for code in codes if normalize_text(code)] + if not normalized_codes: + return {} + in_clause, params = build_in_clause("actual_sga_code", normalized_codes) + with engine.begin() as conn: + rows = conn.execute( + text( + f""" + SELECT + support_dept_code, + COALESCE(label, '') AS label, + COALESCE(reference, '') AS reference, + COALESCE(note, '') AS note, + COALESCE(grade, '') AS grade, + COALESCE(minutes, '') AS minutes, + COALESCE(amount, 0) AS amount + FROM project_actual_input_entries + WHERE support_dept_code IN ({in_clause}) + AND COALESCE(group_name, '') = 'sga' + ORDER BY position, id + """ + ), + params, + ).mappings().all() + + grouped: dict[str, list[dict[str, Any]]] = {} + for row in rows: + code = normalize_text(row.get("support_dept_code")) + if not code: + continue + grouped.setdefault(code, []).append(dict(row)) + + result: dict[str, dict[str, Any]] = {} + for code, code_rows in grouped.items(): + total_amount = sum(normalize_amount(item.get("amount")) for item in code_rows) + has_detail_trace = any( + normalize_text(item.get("reference")) + or normalize_text(item.get("note")) + or normalize_text(item.get("grade")) + or normalize_text(item.get("minutes")) + for item in code_rows + ) + distinct_labels = { + normalize_text(item.get("label")) + for item in code_rows + if normalize_text(item.get("label")) + } + result[code] = { + "rows": code_rows, + "amount": total_amount, + "has_detail_trace": has_detail_trace, + "distinct_labels": distinct_labels, + } + return result + + +def _get_project_exec_budget_summary(codes: list[str]) -> dict[str, dict[str, float]]: + normalized_codes = [normalize_text(code) for code in codes if normalize_text(code)] + if not normalized_codes: + return {} + in_clause, params = build_in_clause("exec_budget_code", normalized_codes) + with engine.begin() as conn: + rows = conn.execute( + text( + f""" + SELECT + support_dept_code, + COALESCE(group_name, '') AS group_name, + SUM(COALESCE(amount, 0)) AS amount + FROM project_exec_budget_entries + WHERE support_dept_code IN ({in_clause}) + GROUP BY support_dept_code, group_name + """ + ), + params, + ).mappings().all() + + result: dict[str, dict[str, float]] = {} + for row in rows: + code = normalize_text(row.get("support_dept_code")) + group_name = normalize_text(row.get("group_name")) + if not code: + continue + current = result.setdefault(code, {"labor": 0.0, "outsource": 0.0, "cost_plan": 0.0}) + current[group_name or "labor"] = normalize_amount(row.get("amount")) + return result + + +def _is_real_project_actual_sga( + actual_sga_summary: dict[str, Any] | None, + expected_sga_budget: float, +) -> bool: + if not actual_sga_summary: + return False + amount = normalize_amount(actual_sga_summary.get("amount")) + if amount <= 0: + return False + if actual_sga_summary.get("has_detail_trace"): + return True + distinct_labels = set(actual_sga_summary.get("distinct_labels") or []) + if len(distinct_labels) > 1: + return True + if distinct_labels and distinct_labels != {"판관비"}: + return True + if expected_sga_budget > 0 and abs(amount - expected_sga_budget) <= 0.5: + return False + return True + + 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( @@ -5427,11 +5601,10 @@ def _get_hanmac_process_cost_tx_by_code(selected_year: int | None) -> dict[str, 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: @@ -5448,18 +5621,18 @@ def _get_hanmac_process_cost_tx_by_code(selected_year: int | None) -> dict[str, return result -def get_process_cost_project_options(source: str, selected_year: int | None, include_related: bool = False) -> list[dict[str, Any]]: +def get_process_cost_project_options( + source: str, + start_year: int | None, + end_year: int | None, + include_related: bool = False, +) -> list[dict[str, Any]]: normalized_source = normalize_text(source).lower() contract_meta = _get_project_contract_meta() billing_summary_map = get_project_billing_summary_map() if normalized_source != "wehago" else {} if normalized_source == "wehago": init_wehago_compare_db(engine) - year_clause = "" - params: dict[str, Any] = {} - if selected_year: - year_clause = "AND fiscal_year = :selected_year" - params["selected_year"] = selected_year with engine.begin() as conn: rows = conn.execute( text( @@ -5480,7 +5653,6 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc FROM wehago_voucher_rows WHERE COALESCE(support_dept_code, '') <> '' AND support_dept_code NOT IN ('ZZZZZZ') - {year_clause} ) SELECT support_dept_code, @@ -5494,10 +5666,10 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc ORDER BY expense_amount DESC, support_dept_code """ ), - params, + {}, ).mappings().all() else: - tx_by_code = _get_hanmac_process_cost_tx_by_code(selected_year) + tx_by_code = _get_hanmac_process_cost_tx_by_code(None) universe_codes = sorted(set(contract_meta) | set(billing_summary_map) | set(tx_by_code)) rows = [] for code in universe_codes: @@ -5525,6 +5697,14 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc continue contract_row = contract_meta.get(code, {}) billing_row = billing_summary_map.get(code, {}) + project_start_date = normalize_text(contract_row.get("project_start_date")) + project_start_year = _extract_year_from_project_code(code) + if start_year and project_start_year and project_start_year < start_year: + continue + if end_year and project_start_year and project_start_year > end_year: + continue + if (start_year or end_year) and not project_start_year: + continue contract_amount = normalize_amount(contract_row.get("hanmac_contract_amount")) revenue_amount = normalize_amount(row.get("revenue_amount")) expense_amount = normalize_amount(row.get("expense_amount")) @@ -5541,10 +5721,12 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc "profit_rate": _safe_ratio(profit_amount, revenue_amount), "voucher_count": int(row.get("voucher_count") or 0), "last_posting_date": normalize_text(row.get("last_posting_date")), + "project_start_date": project_start_date, + "project_kind_code": _get_process_cost_project_kind(code)[0], + "project_kind_label": _get_process_cost_project_kind(code)[1], } ) - 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 _sort_process_cost_project_options(result) def get_process_cost_project_detail( @@ -5568,14 +5750,12 @@ def get_process_cost_project_detail( 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] + actual_sga_summary_map = _get_project_actual_sga_summary(cluster_codes) + exec_budget_summary_map = _get_project_exec_budget_summary(cluster_codes) if normalized_source == "wehago": init_wehago_compare_db(engine) in_clause, code_params = build_in_clause("process_cost_wehago_code", cluster_codes) - year_clause = "" params: dict[str, Any] = dict(code_params) - if selected_year: - year_clause = "AND fiscal_year = :selected_year" - params["selected_year"] = selected_year amount_expr = ( "CASE " "WHEN ABS(COALESCE(compare_amount, 0)) > 0 THEN ABS(COALESCE(compare_amount, 0)) " @@ -5599,7 +5779,6 @@ def get_process_cost_project_detail( MAX(COALESCE(proof_date, '')) AS last_posting_date FROM wehago_voucher_rows WHERE support_dept_code IN ({in_clause}) - {year_clause} """ ), params, @@ -5616,7 +5795,6 @@ def get_process_cost_project_detail( FROM wehago_voucher_rows WHERE support_dept_code IN ({in_clause}) AND (account_code LIKE '5%' OR account_code LIKE '6%') - {year_clause} GROUP BY account_code, account_name ORDER BY amount DESC, account_code LIMIT 14 @@ -5633,7 +5811,6 @@ def get_process_cost_project_detail( 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 WHERE support_dept_code IN ({in_clause}) - {year_clause} AND LENGTH(COALESCE(proof_date, '')) >= 7 GROUP BY month_label ORDER BY month_label DESC @@ -5644,11 +5821,7 @@ def get_process_cost_project_detail( ).mappings().all() else: in_clause, code_params = build_in_clause("process_cost_code", cluster_codes) - year_clause = "" params = dict(code_params) - if selected_year: - year_clause = "AND year = :selected_year" - params["selected_year"] = selected_year with engine.begin() as conn: summary = conn.execute( text( @@ -5665,7 +5838,6 @@ def get_process_cost_project_detail( MAX(COALESCE(posting_date, '')) AS last_posting_date FROM transactions WHERE support_dept_code IN ({in_clause}) - {year_clause} """ ), params, @@ -5682,7 +5854,6 @@ def get_process_cost_project_detail( FROM transactions WHERE support_dept_code IN ({in_clause}) AND (account_code LIKE '5%' OR account_code LIKE '6%') - {year_clause} GROUP BY account_code, account_name ORDER BY amount DESC, account_code LIMIT 14 @@ -5699,7 +5870,6 @@ def get_process_cost_project_detail( SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN amount ELSE 0 END) AS expense_amount FROM transactions WHERE support_dept_code IN ({in_clause}) - {year_clause} AND year IS NOT NULL AND month IS NOT NULL GROUP BY year, month @@ -5714,24 +5884,46 @@ def get_process_cost_project_detail( 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) + expected_sga_budget = sum(normalize_amount(contract_meta_map.get(member, {}).get("expected_sga_budget")) for member in cluster_codes) + planned_labor_amount = sum(normalize_amount((exec_budget_summary_map.get(member) or {}).get("labor")) for member in cluster_codes) + planned_outsource_amount = sum(normalize_amount((exec_budget_summary_map.get(member) or {}).get("outsource")) for member in cluster_codes) + planned_cost_plan_amount = sum(normalize_amount((exec_budget_summary_map.get(member) or {}).get("cost_plan")) 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")) + expected_sga_budget = normalize_amount(contract_meta.get("expected_sga_budget")) + planned_labor_amount = normalize_amount((exec_budget_summary_map.get(code) or {}).get("labor")) + planned_outsource_amount = normalize_amount((exec_budget_summary_map.get(code) or {}).get("outsource")) + planned_cost_plan_amount = normalize_amount((exec_budget_summary_map.get(code) or {}).get("cost_plan")) revenue_amount = normalize_amount(summary_row.get("revenue_amount")) - expense_amount = normalize_amount(summary_row.get("expense_amount")) + ledger_expense_amount = normalize_amount(summary_row.get("expense_amount")) labor_amount = normalize_amount(summary_row.get("labor_amount")) outsourcing_amount = normalize_amount(summary_row.get("outsourcing_amount")) - sga_amount = normalize_amount(summary_row.get("sga_amount")) - design_cost_amount = max(expense_amount - labor_amount - outsourcing_amount - sga_amount, 0.0) + ledger_sga_amount = normalize_amount(summary_row.get("sga_amount")) + real_project_actual_sga_amount = 0.0 + for member in cluster_codes: + member_expected_sga_budget = normalize_amount(contract_meta_map.get(member, {}).get("expected_sga_budget")) + actual_summary = actual_sga_summary_map.get(member) + if _is_real_project_actual_sga(actual_summary, member_expected_sga_budget): + real_project_actual_sga_amount += normalize_amount((actual_summary or {}).get("amount")) + sga_amount = ledger_sga_amount + real_project_actual_sga_amount + expense_amount = ledger_expense_amount + real_project_actual_sga_amount + design_cost_amount = max(ledger_expense_amount - labor_amount - outsourcing_amount - ledger_sga_amount, 0.0) profit_amount = revenue_amount - expense_amount - target_base = contract_amount * 0.85 if contract_amount > 0 else max(revenue_amount * 0.85, expense_amount) + target_base = ( + planned_labor_amount + + planned_outsource_amount + + planned_cost_plan_amount + + as_cost_amount + + expected_sga_budget + ) phase_rows = [ - {"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.23, "actual_amount": design_cost_amount}, + {"phase": "직접인건비", "target_amount": planned_labor_amount, "actual_amount": labor_amount}, + {"phase": "외주비", "target_amount": planned_outsource_amount, "actual_amount": outsourcing_amount}, + {"phase": "제경비", "target_amount": planned_cost_plan_amount, "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": expected_sga_budget, "actual_amount": sga_amount}, ] for row in phase_rows: row["gap_amount"] = row["target_amount"] - row["actual_amount"] @@ -5750,6 +5942,19 @@ def get_process_cost_project_detail( "last_posting_date": normalize_text(row.get("last_posting_date")), } ) + if real_project_actual_sga_amount > 0: + normalized_accounts.append( + { + "account_code": "PROJECT-SGA", + "account_name": "판관비(프로젝트 정보)", + "amount": real_project_actual_sga_amount, + "row_count": sum(len((actual_sga_summary_map.get(member) or {}).get("rows") or []) for member in cluster_codes), + "share_rate": _safe_ratio(real_project_actual_sga_amount, expense_amount), + "last_posting_date": "", + } + ) + normalized_accounts.sort(key=lambda item: (-normalize_amount(item.get("amount")), normalize_text(item.get("account_code")))) + normalized_accounts = normalized_accounts[:14] normalized_monthly = [] for row in monthly_rows: @@ -5781,6 +5986,9 @@ def get_process_cost_project_detail( "voucher_count": int(summary_row.get("voucher_count") or 0), "row_count": int(summary_row.get("row_count") or 0), "included_codes": cluster_codes if include_related else [code], + "expected_sga_budget": expected_sga_budget, + "ledger_sga_amount": ledger_sga_amount, + "project_actual_sga_amount": real_project_actual_sga_amount, }, "phase_rows": phase_rows, "account_rows": normalized_accounts, @@ -5798,7 +6006,8 @@ def get_process_cost_project_detail( def render_process_cost_page( request: Request, source: str | None = None, - year: int | None = None, + start_year: int | None = None, + end_year: int | None = None, code: str | None = None, include_related: bool = False, message: str = "", @@ -5810,17 +6019,39 @@ def render_process_cost_page( normalized_source = "hanmac" years = get_process_cost_available_years(normalized_source) - selected_year = year if year in years else None - project_options = get_process_cost_project_options(normalized_source, selected_year, include_related=include_related) + selected_start_year = start_year if start_year in years else None + selected_end_year = end_year if end_year in years else None + if not selected_start_year and not selected_end_year and years: + selected_start_year = years[0] + selected_end_year = years[-1] + elif selected_start_year and selected_end_year and selected_start_year > selected_end_year: + selected_end_year = selected_start_year + elif selected_end_year and not selected_start_year: + selected_start_year = years[0] if years else None + if selected_start_year and selected_start_year > selected_end_year: + selected_start_year = selected_end_year + elif selected_start_year and not selected_end_year: + selected_end_year = years[-1] if years else None + if selected_end_year and selected_end_year < selected_start_year: + selected_end_year = selected_start_year + + project_options = get_process_cost_project_options( + normalized_source, + selected_start_year, + selected_end_year, + include_related=include_related, + ) selected_code = normalize_text(code) if selected_code and not any(item["support_dept_code"] == selected_code for item in project_options): selected_code = "" - if not selected_code and project_options: - selected_code = project_options[0]["support_dept_code"] + selected_project = next( + (item for item in project_options if normalize_text(item.get("support_dept_code")) == selected_code), + None, + ) detail = get_process_cost_project_detail( normalized_source, - selected_year, + None, selected_code, include_related=include_related, ) @@ -5828,12 +6059,16 @@ def render_process_cost_page( **base_context(request, message), "process_cost_source": normalized_source, "process_cost_years": years, - "process_cost_selected_year": selected_year, + "process_cost_years_desc": sorted(years, reverse=True), + "process_cost_selected_start_year": selected_start_year, + "process_cost_selected_end_year": selected_end_year, "process_cost_selected_code": selected_code, + "process_cost_selected_project": selected_project, "process_cost_include_related": include_related, "process_cost_projects": project_options, "process_cost_detail": detail, "process_cost_related_codes": get_process_cost_related_codes(selected_code), + "process_cost_quick_link_codes": get_process_cost_quick_links(), } return templates.TemplateResponse(request, "process_cost.html", context) @@ -6832,6 +7067,31 @@ async def project_quick_links_save(request: Request): return JSONResponse(content={"error": str(exc)}, status_code=500) +@app.get("/process-cost/quick-links") +async def process_cost_quick_links_load(): + try: + return JSONResponse(content={"codes": get_process_cost_quick_links()}) + except Exception as exc: + logger.exception("프로세스 원가 바로가기 조회 에러: %s", exc) + return JSONResponse(content={"error": str(exc)}, status_code=500) + + +@app.post("/process-cost/quick-links") +async def process_cost_quick_links_save(request: Request): + try: + payload = await request.json() + if not isinstance(payload, dict): + raise ValueError("잘못된 바로가기 형식입니다.") + codes = payload.get("codes") or [] + if not isinstance(codes, list): + raise ValueError("바로가기 목록 형식이 잘못되었습니다.") + save_process_cost_quick_links([normalize_text(code) for code in codes if isinstance(code, str)]) + return JSONResponse(content={"status": "ok", "codes": get_process_cost_quick_links()}) + except Exception as exc: + logger.exception("프로세스 원가 바로가기 저장 에러: %s", exc) + return JSONResponse(content={"error": str(exc)}, status_code=500) + + @app.post("/projects/uncontracted-category") async def project_uncontracted_category_save(request: Request): try: @@ -7214,7 +7474,8 @@ async def annual_summary(request: Request): async def process_cost( request: Request, source: str | None = None, - year: str | None = None, + start_year: str | None = None, + end_year: str | None = None, code: str | None = None, include_related: str | None = None, ): @@ -7222,7 +7483,8 @@ async def process_cost( return render_process_cost_page( request, source=source, - year=parse_optional_year(year), + start_year=parse_optional_year(start_year), + end_year=parse_optional_year(end_year), code=code, include_related=normalize_text(include_related) in {"1", "true", "y", "yes", "on"}, ) diff --git a/templates/annual_summary.html b/templates/annual_summary.html index dced093..48c654f 100644 --- a/templates/annual_summary.html +++ b/templates/annual_summary.html @@ -54,13 +54,12 @@ } .chart-box { - background: - linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,247,249,0.98)), - radial-gradient(circle at top left, rgba(17, 17, 17, 0.045), transparent 36%); - border: 1px solid var(--line); - border-radius: 16px; - padding: 18px 20px 20px; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.92); + background: transparent; + border: 0; + border-top: 1px solid var(--line); + border-radius: 0; + padding: 18px 0 0; + box-shadow: none; content-visibility: auto; contain-intrinsic-size: 620px; } @@ -96,6 +95,8 @@ min-height: 100%; content-visibility: auto; contain-intrinsic-size: 760px; + padding-right: 14px; + border-right: 1px solid rgba(217, 221, 227, 0.9); } .metric-grid { @@ -128,8 +129,8 @@ } .summary-overview .stat-card { - padding: 14px 14px 15px; - border-radius: 12px; + padding: 0 8px 12px 0; + border-radius: 0; gap: 4px; } @@ -174,7 +175,7 @@ } body[data-view-mode="dual"] .chart-box { - padding: 12px 12px 14px; + padding: 12px 0 0; min-height: 100%; } @@ -191,6 +192,13 @@ .metric-grid { grid-template-columns: 1fr; } + + .summary-overview { + padding-right: 0; + padding-bottom: 12px; + border-right: 0; + border-bottom: 1px solid rgba(217, 221, 227, 0.9); + } } {% endblock %} diff --git a/templates/base.html b/templates/base.html index 213da8d..41cbefb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,6 +23,7 @@ --page-frame-width-dual: min(2560px, calc(100vw - (var(--page-gutter) * 2))); --page-frame-width-single: min(1520px, calc(100vw - (var(--page-gutter) * 2))); --status-widget-height: 34px; + --toolbar-row-height: 44px; } * { @@ -116,12 +117,12 @@ } .panel { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 18px; - padding: var(--panel-pad); - box-shadow: 0 10px 24px rgba(21, 24, 29, 0.045); - backdrop-filter: blur(6px); + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + box-shadow: none; + backdrop-filter: none; } .section-title { @@ -165,11 +166,12 @@ } .stat-card { - background: var(--white); - border: 1px solid var(--line); - border-radius: 14px; - padding: 14px 15px 13px; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); + background: transparent; + border: 0; + border-radius: 0; + padding: 0 0 14px; + box-shadow: none; + border-bottom: 1px solid rgba(217, 221, 227, 0.9); } .stat-card .label { @@ -402,9 +404,10 @@ .table-wrap { overflow: auto; - border: 1px solid var(--line); - border-radius: 14px; - background: var(--white); + border: 0; + border-top: 1px solid var(--line); + border-radius: 0; + background: transparent; } table { @@ -431,7 +434,7 @@ } tbody tr:nth-child(even) td { - background: var(--table-alt); + background: rgba(245, 246, 248, 0.65); } .empty { @@ -614,9 +617,9 @@