Files
HM_project_Viewer_Board/templates/wehago_benefit_entertainment.html

922 lines
37 KiB
HTML

{% extends "base.html" %}
{% block title %}복리/접대비 분류{% endblock %}
{% block content %}
{% set filters = benefit_report.filters %}
<style>
.report-head {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 18px;
padding-bottom: 14px;
border-bottom: 1px solid var(--line);
}
.report-head h1 {
font-size: 26px;
line-height: 1.2;
}
.report-head p {
margin-top: 6px;
color: var(--muted);
font-size: 13px;
}
.report-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.report-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 0 14px;
border: 1px solid #111;
background: #111;
color: #fff;
border-radius: 8px;
text-decoration: none;
font-size: 13px;
font-weight: 800;
white-space: nowrap;
}
.report-filter {
display: grid;
grid-template-columns: 82px 82px 136px 164px 150px 180px 62px;
gap: 10px;
align-items: end;
padding: 14px 0;
border-bottom: 1px solid var(--line);
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
.report-filter .field {
min-width: 0;
}
.report-filter input,
.report-filter select {
width: 100%;
min-height: 38px;
border-radius: 8px;
border: 1px solid var(--line);
background: #fff;
padding: 0 10px;
}
.report-filter button {
min-height: 38px;
border-radius: 8px;
border: 1px solid #111;
background: #111;
color: #fff;
font-weight: 800;
cursor: pointer;
}
.report-kpis {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 14px;
padding: 16px 0 4px;
}
.report-kpi {
border-bottom: 1px solid var(--line);
padding-bottom: 12px;
}
.report-kpi span {
display: block;
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.report-kpi strong {
display: block;
margin-top: 6px;
font-size: 24px;
line-height: 1.15;
}
.report-toggle-kpi {
display: flex;
align-items: flex-end;
justify-content: flex-end;
border-bottom: 1px solid var(--line);
padding-bottom: 12px;
}
.report-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
padding: 0 10px;
border: 1px solid var(--line);
background: #fff;
color: #111;
border-radius: 0;
text-decoration: none;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.report-toggle.is-on {
border-color: #111;
background: #111;
color: #fff;
}
.report-grid {
display: grid;
grid-template-columns: minmax(320px, 0.88fr) minmax(520px, 1.12fr);
gap: 18px;
align-items: start;
margin-top: 18px;
}
.report-summary-band {
margin-top: 16px;
}
.report-section h2 {
font-size: 17px;
margin-bottom: 10px;
}
.report-table-wrap {
overflow: auto;
border-top: 1px solid var(--line);
}
.report-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
line-height: 1.15;
min-width: 860px;
}
.report-table th {
position: sticky;
top: 0;
z-index: 1;
background: #f5f6f8;
color: #4b5563;
text-align: left;
font-size: 11px;
padding: 4px 5px;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.report-table td {
padding: 2px 5px;
border-bottom: 1px solid #eceff3;
vertical-align: middle;
}
.report-table .number {
text-align: right;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.report-table .desc {
min-width: 260px;
line-height: 1.25;
white-space: nowrap;
}
.report-table .counterpart {
min-width: 300px;
max-width: 420px;
line-height: 1.25;
white-space: nowrap;
position: relative;
}
.counterpart-text {
display: block;
max-width: 420px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.counterpart-hover-tooltip {
position: fixed;
display: none;
z-index: 1400;
min-width: 520px;
max-width: min(820px, 72vw);
padding: 8px 10px;
border: 1px solid var(--line);
background: #fff;
color: #111;
white-space: pre-line;
line-height: 1.45;
box-shadow: none;
pointer-events: none;
}
.counterpart-hover-tooltip.is-open {
display: block;
}
#voucherDetailBody td {
white-space: nowrap;
}
.report-table button.link-cell {
all: unset;
display: inline;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: #111;
font: inherit;
font-weight: inherit;
cursor: pointer;
text-decoration: none;
padding: 0 !important;
margin: 0;
min-height: 0;
line-height: inherit;
border-radius: 0 !important;
}
.report-table button.link-cell:hover,
.report-table button.link-cell:focus,
.report-table button.link-cell:active {
background: transparent !important;
box-shadow: none !important;
text-decoration: none !important;
outline: none;
}
.summary-account-group {
display: -webkit-box;
max-width: 58px;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.15;
word-break: keep-all;
}
.summary-person-cell,
.summary-person-cell .link-cell {
display: -webkit-box !important;
max-width: 128px;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.15;
word-break: keep-all;
}
.category-save-cell {
display: block;
min-width: 138px;
}
.category-save-cell select {
min-height: 20px;
border: 1px solid var(--line);
border-radius: 0;
background: #fff;
font-size: 12px;
padding: 0 3px;
width: 100%;
}
.report-pill {
display: inline-flex;
align-items: center;
min-height: 0;
padding: 0;
border-radius: 0;
background: transparent;
font-weight: 800;
white-space: nowrap;
}
.report-muted {
color: var(--muted);
font-size: 12px;
margin-top: 8px;
}
.detail-more-row {
display: flex;
justify-content: center;
padding-top: 10px;
}
.detail-more-row button {
min-height: 30px;
border: 1px solid var(--line);
background: #fff;
border-radius: 0;
padding: 0 12px;
cursor: pointer;
font-weight: 800;
}
.modal-detail-toggle {
margin-top: 12px;
min-height: 30px;
border: 1px solid var(--line);
background: #fff;
border-radius: 0;
padding: 0 12px;
cursor: pointer;
font-weight: 800;
}
.modal-detail-panel {
display: none;
margin-top: 10px;
}
.modal-detail-panel.is-open {
display: block;
}
.report-modal-backdrop {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(17, 24, 39, 0.42);
z-index: 1000;
padding: 20px;
}
.report-modal-backdrop.is-open {
display: flex;
}
.report-modal {
width: min(1680px, calc(100vw - 24px));
max-height: min(860px, calc(100vh - 24px));
background: #fff;
border-radius: 8px;
box-shadow: none;
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
}
.report-modal-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.report-modal-head strong {
font-size: 16px;
}
.report-modal-head button {
border: 1px solid var(--line);
background: #fff;
border-radius: 7px;
min-height: 32px;
padding: 0 10px;
cursor: pointer;
font-weight: 800;
}
.report-modal-body {
overflow: auto;
padding: 12px 16px 16px;
}
.report-modal .report-table {
min-width: 1680px;
}
.report-modal .report-table th,
.report-modal .report-table td {
white-space: nowrap;
}
.report-modal .report-table .desc {
min-width: 460px;
white-space: nowrap;
}
.report-modal .report-table .counterpart {
min-width: 360px;
white-space: nowrap;
}
.report-modal .counterpart-text {
max-width: 560px;
}
@media (max-width: 980px) {
.report-head,
.report-grid {
display: grid;
}
.report-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
<section class="panel">
<div class="report-head">
<div>
<h1>복리후생비/접대비 분류</h1>
<p>{{ filters.start_year }}년부터 {{ filters.end_year }}년까지 WEHAGO 전표 기준</p>
</div>
<div class="report-actions">
<a class="report-button" href="/wehago-benefit-entertainment/export?start_year={{ filters.start_year }}&end_year={{ filters.end_year }}&account_group={{ filters.account_group|urlencode }}&category={{ filters.category|urlencode }}&person_keyword={{ filters.person_keyword|urlencode }}&desc_keyword={{ filters.desc_keyword|urlencode }}&include_adjustments={{ '1' if filters.include_adjustments else '0' }}">엑셀 다운로드</a>
</div>
</div>
<form class="report-filter" method="get" action="/wehago-benefit-entertainment">
<div class="field">
<label for="start_year">시작연도</label>
<input id="start_year" name="start_year" value="{{ filters.start_year }}" inputmode="numeric">
</div>
<div class="field">
<label for="end_year">종료연도</label>
<input id="end_year" name="end_year" value="{{ filters.end_year }}" inputmode="numeric">
</div>
<div class="field">
<label for="account_group">구분</label>
<select id="account_group" name="account_group">
<option value="all" {% if filters.account_group in ['all', ''] %}selected{% endif %}>전체</option>
{% for option in benefit_report.account_group_options %}
<option value="{{ option }}" {% if filters.account_group == option %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="category">분류</label>
<select id="category" name="category">
<option value="all" {% if filters.category in ['all', ''] %}selected{% endif %}>전체</option>
{% for option in benefit_report.category_options %}
<option value="{{ option }}" {% if filters.category == option %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="person_keyword">개인/거래처</label>
<input id="person_keyword" name="person_keyword" value="{{ filters.person_keyword }}" autocomplete="off">
</div>
<div class="field">
<label for="desc_keyword">적요</label>
<input id="desc_keyword" name="desc_keyword" value="{{ filters.desc_keyword }}" autocomplete="off">
</div>
<input type="hidden" name="include_adjustments" value="{{ '1' if filters.include_adjustments else '0' }}">
<button type="submit">검색</button>
</form>
<div class="report-kpis">
<div class="report-kpi"><span>총 금액</span><strong>{{ "{:,.0f}".format(benefit_report.total_amount) }}</strong></div>
<div class="report-kpi"><span>전표 행</span><strong>{{ "{:,}".format(benefit_report.row_count) }}</strong></div>
<div class="report-kpi"><span>거래처 요약</span><strong>{{ "{:,}".format(benefit_report.vendor_summary_rows|length) }}</strong></div>
<div class="report-kpi"><span>직급 확인</span><strong>{{ "{:,}".format(benefit_report.hanmac_grade_match_count) }}</strong></div>
<div class="report-kpi"><span>미리보기</span><strong>{{ "{:,}".format(benefit_report.shown_count) }}</strong></div>
<div class="report-toggle-kpi">
<a class="report-toggle {% if filters.include_adjustments %}is-on{% endif %}" href="/wehago-benefit-entertainment?start_year={{ filters.start_year }}&end_year={{ filters.end_year }}&account_group={{ filters.account_group|urlencode }}&category={{ filters.category|urlencode }}&person_keyword={{ filters.person_keyword|urlencode }}&desc_keyword={{ filters.desc_keyword|urlencode }}&include_adjustments={{ '0' if filters.include_adjustments else '1' }}">
대체/원가/손익 {{ '포함' if filters.include_adjustments else '제외' }}
</a>
</div>
</div>
<section class="report-section report-summary-band">
<h2>분류/연도별 합계</h2>
<div class="report-table-wrap">
<table class="report-table">
<thead>
<tr>
<th>구분</th>
<th>분류</th>
{% for year in benefit_report.summary_years %}
<th class="number">{{ year }}</th>
{% endfor %}
<th class="number">합계</th>
<th class="number">건수</th>
</tr>
</thead>
<tbody>
{% for row in benefit_report.category_year_summary_rows %}
{% set is_drill_category = (row.account_group == '복리후생비' and row.category in ['임원 개인성 비용', '기타', '경조사']) or (row.account_group == '접대비' and row.category in ['기타', '경조사', '운동비']) %}
<tr>
<td>{{ row.account_group }}</td>
<td><span class="report-pill">{{ row.category }}</span></td>
{% for year in benefit_report.summary_years %}
<td class="number">
{% if is_drill_category and row.year_amounts.get(year, 0) %}
<button type="button" class="link-cell" data-category-year-detail data-account-group="{{ row.account_group }}" data-category="{{ row.category }}" data-year="{{ year }}">{{ "{:,.0f}".format(row.year_amounts.get(year, 0)) }}</button>
{% else %}
{{ "{:,.0f}".format(row.year_amounts.get(year, 0)) }}
{% endif %}
</td>
{% endfor %}
<td class="number">
{% if is_drill_category and row.total_amount %}
<button type="button" class="link-cell" data-category-year-detail data-account-group="{{ row.account_group }}" data-category="{{ row.category }}" data-year="all">{{ "{:,.0f}".format(row.total_amount) }}</button>
{% else %}
{{ "{:,.0f}".format(row.total_amount) }}
{% endif %}
</td>
<td class="number">
{% if is_drill_category and row.row_count %}
<button type="button" class="link-cell" data-category-year-detail data-account-group="{{ row.account_group }}" data-category="{{ row.category }}" data-year="all">{{ "{:,}".format(row.row_count) }}</button>
{% else %}
{{ "{:,}".format(row.row_count) }}
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="{{ benefit_report.summary_years|length + 4 }}">조회된 자료가 없습니다.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<div class="report-grid">
<section class="report-section">
<h2>거래처별 요약</h2>
<div class="report-table-wrap">
<table class="report-table">
<thead>
<tr>
<th>구분</th>
<th>분류</th>
<th>거래처</th>
<th>개인/귀속</th>
<th>직급</th>
<th class="number">금액</th>
<th class="number">건수</th>
</tr>
</thead>
<tbody>
{% for row in benefit_report.vendor_summary_rows %}
<tr>
<td><span class="summary-account-group">{{ row.account_group }}</span></td>
<td><span class="report-pill">{{ row.category }}</span></td>
<td>{{ row.vendor_name }}</td>
<td class="summary-person-cell"><button type="button" class="link-cell" data-summary-detail data-field="person" data-vendor="{{ row.vendor_name }}" data-category="{{ row.category }}" data-account-group="{{ row.account_group }}">{{ row.person_name or "-" }}</button></td>
<td>{{ row.hanmac_member_grade or "" }}</td>
<td class="number"><button type="button" class="link-cell" data-summary-detail data-field="amount" data-vendor="{{ row.vendor_name }}" data-category="{{ row.category }}" data-account-group="{{ row.account_group }}">{{ "{:,.0f}".format(row.amount) }}</button></td>
<td class="number"><button type="button" class="link-cell" data-summary-detail data-field="count" data-vendor="{{ row.vendor_name }}" data-category="{{ row.category }}" data-account-group="{{ row.account_group }}">{{ "{:,}".format(row.row_count) }}</button></td>
</tr>
{% else %}
<tr><td colspan="7">조회된 자료가 없습니다.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section class="report-section">
<h2>전표 상세</h2>
<div class="report-table-wrap">
<table class="report-table">
<thead>
<tr>
<th>연도</th>
<th>일자</th>
<th>전표번호</th>
<th>계정</th>
<th>거래처</th>
<th>직급</th>
<th>분류</th>
<th class="number">차변금액</th>
<th class="number">대변금액</th>
<th>적요</th>
<th>상대계정</th>
</tr>
</thead>
<tbody id="voucherDetailBody">
{% for row in benefit_report.detail_rows %}
<tr data-detail-row>
<td>{{ row.year }}</td>
<td>{{ row.ledger_date }}</td>
<td>{{ row.voucher_no }}</td>
<td>{{ row.account_name }}</td>
<td>{{ row.vendor_name }}</td>
<td>{{ row.hanmac_member_grade or "" }}</td>
<td>
<div class="category-save-cell">
<select data-category-select data-row-id="{{ row.ledger_row_id }}">
{% for option in benefit_report.category_options %}
<option value="{{ option }}" {% if row.category == option %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
</div>
</td>
<td class="number">{{ "{:,.0f}".format(row.debit) }}</td>
<td class="number">{{ "{:,.0f}".format(row.credit) }}</td>
<td class="desc">{{ row.description }}</td>
<td class="counterpart" data-tooltip="{{ row.counterpart_tooltip }}"><span class="counterpart-text">{{ row.counterpart_details }}</span></td>
</tr>
{% else %}
<tr><td colspan="11">조회된 자료가 없습니다.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="detail-more-row" {% if benefit_report.row_count <= benefit_report.shown_count %}style="display:none"{% endif %}>
<button type="button" id="voucherDetailMore">더 보기</button>
</div>
<p class="report-muted" id="voucherDetailMeta">화면에는 {{ "{:,}".format(benefit_report.shown_count) }}행이 표시됩니다. 엑셀에는 현재 필터의 전체 {{ "{:,}".format(benefit_report.row_count) }}행이 포함됩니다.</p>
</section>
</div>
</section>
<div class="report-modal-backdrop" id="summaryDetailModal" aria-hidden="true">
<div class="report-modal" role="dialog" aria-modal="true" aria-labelledby="summaryDetailTitle">
<div class="report-modal-head">
<strong id="summaryDetailTitle">상세</strong>
<button type="button" id="summaryDetailClose">닫기</button>
</div>
<div class="report-modal-body" id="summaryDetailBody"></div>
</div>
</div>
<div class="counterpart-hover-tooltip" id="counterpartHoverTooltip" aria-hidden="true"></div>
{% endblock %}
{% block script %}
<script>
(() => {
const reportData = {{ benefit_report_json|tojson }};
const modal = document.getElementById("summaryDetailModal");
const modalTitle = document.getElementById("summaryDetailTitle");
const modalBody = document.getElementById("summaryDetailBody");
const modalClose = document.getElementById("summaryDetailClose");
const detailBody = document.getElementById("voucherDetailBody");
const detailMore = document.getElementById("voucherDetailMore");
const detailMeta = document.getElementById("voucherDetailMeta");
const counterpartTooltip = document.getElementById("counterpartHoverTooltip");
const detailPageSize = 300;
const detailRows = reportData.detail_rows || [];
const categoryOptions = reportData.category_options || Array.from(document.querySelectorAll("[data-category-select] option")).map((option) => option.value);
let detailVisibleCount = document.querySelectorAll("[data-detail-row]").length;
function escapeHtml(value) {
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
}[char]));
}
function formatNumber(value) {
return Number(value || 0).toLocaleString("ko-KR", { maximumFractionDigits: 0 });
}
function displayVendor(value) {
const text = String(value || "").trim();
return text || "미지정";
}
function renderCategoryOptions(selectedCategory) {
return categoryOptions.map((option) => `
<option value="${escapeHtml(option)}" ${String(option) === String(selectedCategory || "") ? "selected" : ""}>${escapeHtml(option)}</option>
`).join("");
}
function renderDetailRow(row) {
return `
<tr data-detail-row>
<td>${escapeHtml(row.year)}</td>
<td>${escapeHtml(row.ledger_date)}</td>
<td>${escapeHtml(row.voucher_no)}</td>
<td>${escapeHtml(row.account_name)}</td>
<td>${escapeHtml(displayVendor(row.vendor_name))}</td>
<td>${escapeHtml(row.hanmac_member_grade || "")}</td>
<td>
<div class="category-save-cell">
<select data-category-select data-row-id="${escapeHtml(row.ledger_row_id)}">
${renderCategoryOptions(row.category)}
</select>
</div>
</td>
<td class="number">${formatNumber(row.debit)}</td>
<td class="number">${formatNumber(row.credit)}</td>
<td class="desc">${escapeHtml(row.description)}</td>
<td class="counterpart" data-tooltip="${escapeHtml(row.counterpart_tooltip || row.counterpart_details)}"><span class="counterpart-text">${escapeHtml(row.counterpart_details)}</span></td>
</tr>
`;
}
function updateDetailControls() {
const totalRows = Number(reportData.row_count || detailRows.length || 0);
if (detailMeta) {
detailMeta.textContent = `화면에는 ${detailVisibleCount.toLocaleString("ko-KR")}행이 표시됩니다. 엑셀에는 현재 필터의 전체 ${totalRows.toLocaleString("ko-KR")}행이 포함됩니다.`;
}
if (detailMore?.parentElement) {
detailMore.parentElement.style.display = detailVisibleCount < detailRows.length ? "flex" : "none";
}
}
function appendDetailRows() {
if (!detailBody) return;
const nextRows = detailRows.slice(detailVisibleCount, detailVisibleCount + detailPageSize);
if (!nextRows.length) {
updateDetailControls();
return;
}
detailBody.insertAdjacentHTML("beforeend", nextRows.map(renderDetailRow).join(""));
detailVisibleCount += nextRows.length;
updateDetailControls();
}
function buildVendorUsageRows(rows) {
const map = new Map();
rows.forEach((row) => {
const vendor = displayVendor(row.vendor_name);
const item = map.get(vendor) || { vendor, amount: 0, count: 0, people: new Set() };
item.amount += Number(row.amount || 0);
item.count += 1;
const person = String(row.person_name || "").trim();
if (person) item.people.add(person);
map.set(vendor, item);
});
return Array.from(map.values()).sort((a, b) => b.amount - a.amount || a.vendor.localeCompare(b.vendor, "ko"));
}
function renderDetailTable(rows, options = {}) {
const showCategory = options.showCategory !== false;
return `
<div class="report-table-wrap">
<table class="report-table">
<thead>
<tr>
<th>일자</th>
<th>전표번호</th>
<th>계정</th>
<th>거래처</th>
<th>개인/귀속</th>
${showCategory ? "<th>분류</th>" : ""}
<th class="number">금액</th>
<th>적요</th>
<th>상대계정</th>
</tr>
</thead>
<tbody>
${rows.map((row) => `
<tr>
<td>${escapeHtml(row.ledger_date)}</td>
<td>${escapeHtml(row.voucher_no)}</td>
<td>${escapeHtml(row.account_name)}</td>
<td>${escapeHtml(displayVendor(row.vendor_name))}</td>
<td>${escapeHtml(row.person_name)}</td>
${showCategory ? `<td>${escapeHtml(row.category)}</td>` : ""}
<td class="number">${formatNumber(row.amount)}</td>
<td class="desc">${escapeHtml(row.description)}</td>
<td class="counterpart" data-tooltip="${escapeHtml(row.counterpart_tooltip || row.counterpart_details)}"><span class="counterpart-text">${escapeHtml(row.counterpart_details)}</span></td>
</tr>
`).join("")}
</tbody>
</table>
</div>
`;
}
function openModal(rows, meta) {
modalTitle.textContent = meta.title || `${meta.vendor || "-"} / ${meta.category || "-"} 상세`;
if (!rows.length) {
modalBody.innerHTML = '<div class="report-muted">상세 값이 없습니다.</div>';
} else {
const total = rows.reduce((sum, row) => sum + Number(row.amount || 0), 0);
if (meta.mode === "categoryYear") {
const vendorRows = buildVendorUsageRows(rows);
modalBody.innerHTML = `
<p class="report-muted">총 ${rows.length.toLocaleString("ko-KR")}건 / ${formatNumber(total)}원</p>
<div class="report-table-wrap">
<table class="report-table">
<thead>
<tr>
<th>거래처</th>
<th>개인/귀속</th>
<th class="number">금액</th>
<th class="number">건수</th>
</tr>
</thead>
<tbody>
${vendorRows.map((row) => `
<tr>
<td>${escapeHtml(row.vendor)}</td>
<td>${escapeHtml(Array.from(row.people).slice(0, 4).join(", ") || "-")}</td>
<td class="number">${formatNumber(row.amount)}</td>
<td class="number">${formatNumber(row.count)}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
<button type="button" class="modal-detail-toggle" id="modalDetailToggle">상세 내역 열기</button>
<div class="modal-detail-panel" id="modalDetailPanel">
${renderDetailTable(rows, { showCategory: false })}
</div>
`;
} else {
modalBody.innerHTML = `
<p class="report-muted">총 ${rows.length.toLocaleString("ko-KR")}건 / ${formatNumber(total)}원</p>
${renderDetailTable(rows)}
`;
}
}
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
}
function closeModal() {
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
}
function positionCounterpartTooltip(event) {
if (!counterpartTooltip || !counterpartTooltip.classList.contains("is-open")) return;
const margin = 14;
const rect = counterpartTooltip.getBoundingClientRect();
let left = event.clientX + margin;
let top = event.clientY + margin;
if (left + rect.width > window.innerWidth - margin) {
left = Math.max(margin, event.clientX - rect.width - margin);
}
if (top + rect.height > window.innerHeight - margin) {
top = Math.max(margin, event.clientY - rect.height - margin);
}
counterpartTooltip.style.left = `${left}px`;
counterpartTooltip.style.top = `${top}px`;
}
document.addEventListener("mouseover", (event) => {
const cell = event.target.closest(".counterpart[data-tooltip]");
if (!cell || !counterpartTooltip) return;
const text = cell.dataset.tooltip || "";
if (!text.trim()) return;
counterpartTooltip.textContent = text;
counterpartTooltip.classList.add("is-open");
counterpartTooltip.setAttribute("aria-hidden", "false");
positionCounterpartTooltip(event);
});
document.addEventListener("mousemove", (event) => {
if (event.target.closest(".counterpart[data-tooltip]")) {
positionCounterpartTooltip(event);
}
});
document.addEventListener("mouseout", (event) => {
const cell = event.target.closest(".counterpart[data-tooltip]");
if (!cell || !counterpartTooltip) return;
if (event.relatedTarget && cell.contains(event.relatedTarget)) return;
counterpartTooltip.classList.remove("is-open");
counterpartTooltip.setAttribute("aria-hidden", "true");
});
modalBody?.addEventListener("click", (event) => {
const toggle = event.target.closest("#modalDetailToggle");
if (!toggle) return;
const panel = document.getElementById("modalDetailPanel");
const isOpen = panel?.classList.toggle("is-open");
toggle.textContent = isOpen ? "상세 내역 접기" : "상세 내역 열기";
});
document.querySelectorAll("[data-summary-detail]").forEach((button) => {
button.addEventListener("click", () => {
const vendor = button.dataset.vendor || "";
const category = button.dataset.category || "";
const accountGroup = button.dataset.accountGroup || "";
const rows = (reportData.detail_rows || []).filter((row) =>
displayVendor(row.vendor_name) === vendor &&
String(row.category || "") === category &&
String(row.account_group || "") === accountGroup
);
openModal(rows, { vendor, category, accountGroup });
});
});
document.querySelectorAll("[data-category-year-detail]").forEach((button) => {
button.addEventListener("click", () => {
const accountGroup = button.dataset.accountGroup || "";
const category = button.dataset.category || "";
const year = button.dataset.year || "all";
const rows = (reportData.detail_rows || []).filter((row) =>
String(row.account_group || "") === accountGroup &&
String(row.category || "") === category &&
(year === "all" || String(row.year || "") === year)
);
const titleYear = year === "all" ? "전체연도" : `${year}년`;
openModal(rows, {
title: `${accountGroup} / ${category} / ${titleYear} 상세`,
vendor: accountGroup,
category,
accountGroup,
mode: "categoryYear",
});
});
});
detailBody?.addEventListener("change", async (event) => {
const select = event.target.closest("[data-category-select]");
if (!select) return;
const rowId = select.dataset.rowId || "";
const category = select.value || "";
select.disabled = true;
try {
const response = await fetch("/wehago-benefit-entertainment/api/category", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ledger_row_id: rowId, category }),
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
throw new Error(payload.error || "분류 저장 중 오류가 발생했습니다.");
}
window.location.reload();
} catch (error) {
alert(error.message || "분류 저장 중 오류가 발생했습니다.");
select.disabled = false;
}
});
detailMore?.addEventListener("click", appendDetailRows);
updateDetailControls();
modalClose?.addEventListener("click", closeModal);
modal?.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeModal();
});
})();
</script>
{% endblock %}