Update project pages and add DB browser
This commit is contained in:
+108
-124
@@ -146,8 +146,7 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pc-quick-link,
|
||||
.pc-related-link {
|
||||
.pc-quick-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -165,8 +164,7 @@
|
||||
}
|
||||
|
||||
.pc-quick-link strong,
|
||||
.pc-quick-link span,
|
||||
.pc-related-link span {
|
||||
.pc-quick-link span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -422,6 +420,10 @@
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.pc-card .value.negative {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.pc-card .meta {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
@@ -826,24 +828,6 @@
|
||||
<div class="pc-selector-field">
|
||||
<input type="text" id="projectSearchInput" class="pc-project-search" placeholder="프로젝트 코드/명 검색" autocomplete="off">
|
||||
<div class="pc-project-dropdown" id="projectList" hidden>
|
||||
{% for item in process_cost_projects %}
|
||||
<a
|
||||
href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}&code={{ item.support_dept_code }}{% if process_cost_include_related %}&include_related=1&active_related={% if process_cost_active_related_codes %}{{ process_cost_active_related_codes|join(',') }}{% else %}-{% endif %}{% 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-kind="{{ item.project_kind_code|lower }}"
|
||||
title="{{ item.support_dept_code }} {{ item.support_dept_name }}"
|
||||
>
|
||||
<div class="pc-project-head">
|
||||
<span class="pc-project-code">{{ item.support_dept_code }}</span>
|
||||
<span class="pc-type-pill">{{ item.project_kind_label }}</span>
|
||||
</div>
|
||||
<div class="pc-project-name">{{ item.support_dept_name }}</div>
|
||||
{% if item.client_name %}
|
||||
<div class="pc-project-name">{{ item.client_name }}</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="pc-note" id="projectListEmpty" hidden style="padding: 12px;">검색 결과가 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -906,7 +890,7 @@
|
||||
</article>
|
||||
<article class="pc-card">
|
||||
<span class="label">영업수지</span>
|
||||
<strong class="value">{{ "{:,.0f}".format(overview.profit_amount|default(0)) }}원</strong>
|
||||
<strong class="value {% if (overview.profit_amount|default(0)) < 0 %}negative{% endif %}">{{ "{:,.0f}".format(overview.profit_amount|default(0)) }}원</strong>
|
||||
<span class="meta">마진율 {{ "{:.1f}".format(overview.profit_rate|default(0)) }}%</span>
|
||||
</article>
|
||||
</div>
|
||||
@@ -935,7 +919,6 @@
|
||||
{% 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>
|
||||
@@ -1065,25 +1048,50 @@
|
||||
<div class="pc-note" id="processFlowModalNote"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script id="processCostPageState" type="application/json">
|
||||
{{ {
|
||||
"projects": process_cost_projects or [],
|
||||
"selectedCode": process_cost_selected_code,
|
||||
"selectedProject": process_cost_selected_project or {},
|
||||
"relatedCodes": process_cost_related_codes or [],
|
||||
"activeRelatedCodes": process_cost_active_related_codes or [],
|
||||
"quickLinkCodes": process_cost_quick_link_codes or [],
|
||||
"source": process_cost_source,
|
||||
"selectedStartYear": process_cost_selected_start_year,
|
||||
"selectedEndYear": process_cost_selected_end_year,
|
||||
"includeRelatedEnabled": process_cost_include_related,
|
||||
"monthlyRows": (process_cost_detail.monthly_rows or []),
|
||||
} | tojson }}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
(() => {
|
||||
const processCostProjects = {{ process_cost_projects | tojson }};
|
||||
const stateNode = document.getElementById("processCostPageState");
|
||||
const pageState = stateNode ? JSON.parse(stateNode.textContent || "{}") : {};
|
||||
const processCostProjects = Array.isArray(pageState.projects) ? pageState.projects : [];
|
||||
const escapeHtml = (value) => String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
const processCostProjectMap = new Map(
|
||||
processCostProjects
|
||||
.filter((item) => item && item.support_dept_code)
|
||||
.map((item) => [item.support_dept_code, item])
|
||||
);
|
||||
const selectedCode = {{ process_cost_selected_code | tojson }};
|
||||
const selectedProject = {{ (process_cost_selected_project or {}) | tojson }};
|
||||
const initialRelatedCodes = {{ (process_cost_related_codes or []) | tojson }};
|
||||
const initialActiveRelatedCodes = {{ (process_cost_active_related_codes or []) | tojson }};
|
||||
const initialQuickLinkCodes = {{ (process_cost_quick_link_codes or []) | tojson }};
|
||||
const selectedCode = typeof pageState.selectedCode === "string" ? pageState.selectedCode : "";
|
||||
const selectedProject = pageState.selectedProject && typeof pageState.selectedProject === "object"
|
||||
? pageState.selectedProject
|
||||
: {};
|
||||
const initialRelatedCodes = Array.isArray(pageState.relatedCodes) ? pageState.relatedCodes : [];
|
||||
const initialActiveRelatedCodes = Array.isArray(pageState.activeRelatedCodes) ? pageState.activeRelatedCodes : [];
|
||||
const initialQuickLinkCodes = Array.isArray(pageState.quickLinkCodes) ? pageState.quickLinkCodes : [];
|
||||
const searchInput = document.getElementById("projectSearchInput");
|
||||
const list = document.getElementById("projectList");
|
||||
const projectListEmpty = document.getElementById("projectListEmpty");
|
||||
let projectListEmpty = document.getElementById("projectListEmpty");
|
||||
const typeFilter = document.getElementById("projectTypeFilter");
|
||||
const quickLinksContainer = document.getElementById("pcQuickLinks");
|
||||
const selectedProjectBookmark = document.getElementById("selectedProjectBookmark");
|
||||
@@ -1093,18 +1101,21 @@
|
||||
const processModalNote = document.getElementById("processFlowModalNote");
|
||||
const processModalClose = document.getElementById("processFlowModalClose");
|
||||
const processButtons = document.querySelectorAll("[data-process-modal]");
|
||||
const includeRelatedEnabled = {{ 1 if process_cost_include_related else 0 }};
|
||||
const selectedStartYear = pageState.selectedStartYear;
|
||||
const selectedEndYear = pageState.selectedEndYear;
|
||||
const selectedSource = typeof pageState.source === "string" ? pageState.source : "hanmac";
|
||||
const includeRelatedEnabled = Boolean(pageState.includeRelatedEnabled);
|
||||
let bookmarkedCodes = Array.isArray(initialQuickLinkCodes) ? [...initialQuickLinkCodes] : [];
|
||||
let activeRelatedCodes = Array.isArray(initialActiveRelatedCodes) ? [...initialActiveRelatedCodes] : [];
|
||||
|
||||
function buildProcessCostUrl(code, options = {}) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("source", {{ process_cost_source | tojson }});
|
||||
if ({{ process_cost_selected_start_year | tojson }}) {
|
||||
params.set("start_year", {{ process_cost_selected_start_year | tojson }});
|
||||
params.set("source", selectedSource);
|
||||
if (selectedStartYear) {
|
||||
params.set("start_year", String(selectedStartYear));
|
||||
}
|
||||
if ({{ process_cost_selected_end_year | tojson }}) {
|
||||
params.set("end_year", {{ process_cost_selected_end_year | tojson }});
|
||||
if (selectedEndYear) {
|
||||
params.set("end_year", String(selectedEndYear));
|
||||
}
|
||||
if (code) {
|
||||
params.set("code", code);
|
||||
@@ -1128,6 +1139,44 @@
|
||||
.filter((item) => item && item.support_dept_code);
|
||||
}
|
||||
|
||||
function getFilteredProjectItems() {
|
||||
const keyword = (searchInput?.value || "").trim().toLowerCase();
|
||||
const typeValue = (typeFilter?.value || "").trim().toLowerCase();
|
||||
const bookmarkedSet = new Set(bookmarkedCodes);
|
||||
return processCostProjects
|
||||
.filter((item) => item && item.support_dept_code)
|
||||
.filter((item) => {
|
||||
const text = `${item.support_dept_code} ${item.support_dept_name || ""} ${item.client_name || ""}`.toLowerCase();
|
||||
const kind = String(item.project_kind_code || "").toLowerCase();
|
||||
const matchesKeyword = !keyword || text.includes(keyword);
|
||||
const matchesType = !typeValue || kind === typeValue;
|
||||
return matchesKeyword && matchesType && !bookmarkedSet.has(item.support_dept_code);
|
||||
});
|
||||
}
|
||||
|
||||
function renderProjectListItems() {
|
||||
if (!list) return 0;
|
||||
const items = getFilteredProjectItems();
|
||||
list.innerHTML = `${items.map((item) => `
|
||||
<a
|
||||
href="${buildProcessCostUrl(item.support_dept_code)}"
|
||||
class="pc-project-item ${selectedCode === item.support_dept_code ? "active" : ""}"
|
||||
data-project-search="${escapeHtml(`${item.support_dept_code} ${item.support_dept_name || ""} ${item.client_name || ""}`.toLowerCase())}"
|
||||
data-project-kind="${escapeHtml(String(item.project_kind_code || "").toLowerCase())}"
|
||||
title="${escapeHtml(`${item.support_dept_code} ${item.support_dept_name || ""}`.trim())}"
|
||||
>
|
||||
<div class="pc-project-head">
|
||||
<span class="pc-project-code">${escapeHtml(item.support_dept_code)}</span>
|
||||
<span class="pc-type-pill">${escapeHtml(item.project_kind_label || "")}</span>
|
||||
</div>
|
||||
<div class="pc-project-name">${escapeHtml(item.support_dept_name || "")}</div>
|
||||
${item.client_name ? `<div class="pc-project-name">${escapeHtml(item.client_name)}</div>` : ""}
|
||||
</a>
|
||||
`).join("")}<div class="pc-note" id="projectListEmpty" ${items.length ? "hidden" : ""} style="padding: 12px;">검색 결과가 없습니다.</div>`;
|
||||
projectListEmpty = document.getElementById("projectListEmpty");
|
||||
return items.length;
|
||||
}
|
||||
|
||||
async function persistBookmarkedCodes(nextCodes) {
|
||||
const response = await fetch("/process-cost/quick-links", {
|
||||
method: "POST",
|
||||
@@ -1171,10 +1220,10 @@
|
||||
return;
|
||||
}
|
||||
quickLinksContainer.innerHTML = items.map((item) => `
|
||||
<div class="pc-quick-link" title="${item.support_dept_code} ${item.support_dept_name || ""}">
|
||||
<div class="pc-quick-link" title="${escapeHtml(item.support_dept_code)} ${escapeHtml(item.support_dept_name || "")}">
|
||||
<a href="${buildProcessCostUrl(item.support_dept_code)}" class="pc-quick-link-main">
|
||||
<strong>${item.support_dept_code}</strong>
|
||||
<span>${item.support_dept_name || ""}</span>
|
||||
<strong>${escapeHtml(item.support_dept_code)}</strong>
|
||||
<span>${escapeHtml(item.support_dept_name || "")}</span>
|
||||
</a>
|
||||
<button type="button" class="pc-bookmark-button is-active" data-bookmark-toggle="${item.support_dept_code}" aria-label="바로가기 해제" title="바로가기 해제">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -1205,42 +1254,16 @@
|
||||
syncSelectedProjectCard();
|
||||
}
|
||||
|
||||
function getVisibleProjectItems() {
|
||||
return [...list.querySelectorAll("[data-project-search]")].filter((item) => item.style.display !== "none");
|
||||
}
|
||||
|
||||
function filterProjectList() {
|
||||
if (!searchInput || !list) return;
|
||||
const keyword = (searchInput.value || "").trim().toLowerCase();
|
||||
const typeValue = (typeFilter?.value || "").trim().toLowerCase();
|
||||
const bookmarkedSet = new Set(bookmarkedCodes);
|
||||
let visibleCount = 0;
|
||||
list.querySelectorAll("[data-project-search]").forEach((item) => {
|
||||
const text = (item.dataset.projectSearch || "").toLowerCase();
|
||||
const kind = (item.dataset.projectKind || "").toLowerCase();
|
||||
const matchesKeyword = !keyword || text.includes(keyword);
|
||||
const matchesType = !typeValue || kind === typeValue;
|
||||
const codeNode = item.querySelector(".pc-project-code");
|
||||
const code = (codeNode?.textContent || "").trim();
|
||||
const isVisible = matchesKeyword && matchesType && !bookmarkedSet.has(code);
|
||||
item.hidden = !isVisible;
|
||||
if (isVisible) {
|
||||
visibleCount += 1;
|
||||
}
|
||||
});
|
||||
if (projectListEmpty) {
|
||||
projectListEmpty.hidden = visibleCount !== 0;
|
||||
}
|
||||
return visibleCount;
|
||||
return renderProjectListItems();
|
||||
}
|
||||
|
||||
function openProjectList() {
|
||||
if (!list) return;
|
||||
const visibleCount = filterProjectList();
|
||||
const visibleItems = getVisibleProjectItems();
|
||||
list.hidden = false;
|
||||
if (projectListEmpty) {
|
||||
projectListEmpty.hidden = (visibleCount || visibleItems.length) !== 0;
|
||||
projectListEmpty.hidden = visibleCount !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1375,7 +1398,7 @@
|
||||
<a
|
||||
href="${buildProcessCostUrl(code, { activeRelatedCodes: openActiveCodes, includeRelated: includeRelatedEnabled })}"
|
||||
class="pc-chip-link"
|
||||
>${code}</a>
|
||||
>${escapeHtml(code)}</a>
|
||||
<button
|
||||
type="button"
|
||||
class="pc-related-include ${isActive ? "is-active" : ""}"
|
||||
@@ -1385,7 +1408,6 @@
|
||||
title="${isActive ? "합산 제외" : "합산 포함"}"
|
||||
${includeRelatedEnabled ? "" : "disabled"}
|
||||
>${isActive ? "−" : "+"}</button>
|
||||
<button type="button" class="pc-related-remove" data-related-remove="${code}">x</button>
|
||||
</span>
|
||||
`;
|
||||
}).join("");
|
||||
@@ -1406,16 +1428,6 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
relatedChipRow.querySelectorAll("[data-related-remove]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const code = button.dataset.relatedRemove || "";
|
||||
if (code) {
|
||||
relatedSet.delete(code);
|
||||
activeRelatedCodes = activeRelatedCodes.filter((item) => item !== code);
|
||||
renderRelatedChips();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderRelatedSuggestions(keyword) {
|
||||
@@ -1438,20 +1450,13 @@
|
||||
}
|
||||
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 || ""}
|
||||
${escapeHtml(item.support_dept_code)} ${escapeHtml(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();
|
||||
}
|
||||
window.alert("연계 프로젝트 링크는 DB 자동 규칙으로만 계산됩니다.");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1463,47 +1468,26 @@
|
||||
relatedSearchInput.addEventListener("focus", () => {
|
||||
renderRelatedSuggestions(relatedSearchInput.value || "");
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (target.closest(".pc-related-form")) return;
|
||||
relatedSuggestList?.classList.remove("active");
|
||||
});
|
||||
}
|
||||
|
||||
if (relatedResetButton) {
|
||||
relatedResetButton.addEventListener("click", () => {
|
||||
relatedSet.clear();
|
||||
for (const code of initialRelatedCodes || []) {
|
||||
relatedSet.add(code);
|
||||
}
|
||||
activeRelatedCodes = Array.isArray(initialActiveRelatedCodes) ? [...initialActiveRelatedCodes] : [];
|
||||
if (relatedSearchInput) relatedSearchInput.value = "";
|
||||
if (relatedSuggestList) {
|
||||
relatedSuggestList.classList.remove("active");
|
||||
relatedSuggestList.innerHTML = "";
|
||||
}
|
||||
renderRelatedChips();
|
||||
window.location.href = buildProcessCostUrl(selectedCode, {
|
||||
includeRelated: includeRelatedEnabled,
|
||||
activeRelatedCodes: Array.isArray(initialRelatedCodes) ? [...initialRelatedCodes] : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
relatedSaveButton.addEventListener("click", () => {
|
||||
window.alert("연계 프로젝트 링크는 DB 자동 규칙으로만 계산됩니다.");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1546,7 +1530,7 @@
|
||||
syncYearRangeOptions();
|
||||
}
|
||||
|
||||
const monthlyRows = {{ (process_cost_detail.monthly_rows or []) | tojson }};
|
||||
const monthlyRows = Array.isArray(pageState.monthlyRows) ? pageState.monthlyRows : [];
|
||||
const svg = document.getElementById("processMonthlyChart");
|
||||
if (!svg || !Array.isArray(monthlyRows) || !monthlyRows.length) {
|
||||
return;
|
||||
@@ -1571,7 +1555,7 @@
|
||||
|
||||
let labels = "";
|
||||
monthlyRows.forEach((row, i) => {
|
||||
labels += `<text x="${x(i)}" y="${height - 12}" text-anchor="middle" fill="#677182" font-size="10">${(row.month_label || '').slice(2)}</text>`;
|
||||
labels += `<text x="${x(i)}" y="${height - 12}" text-anchor="middle" fill="#677182" font-size="10">${escapeHtml((row.month_label || "").slice(2))}</text>`;
|
||||
});
|
||||
|
||||
svg.innerHTML = `
|
||||
|
||||
Reference in New Issue
Block a user